Chapter 8. Peer-to-Peer Over Bluetooth Using GameKit

One of the coolest new frameworks added to the iPhone 3 SDK is called GameKit. GameKit makes it easy to wirelessly connect multiple iPhones or iPod touches using Bluetooth. Bluetooth is a wireless networking option built into all but the first-generation iPhone and iPod touch. GameKit allows any supported devices to communicate with any other supported devices that are within roughly 30 feet (about 10 meters) of each other. Though the name implies differently, GameKit is useful for nongaming apps, too. For example, you might build a social networking app that allows people to easily transfer contact information over Bluetooth.

Warning

The code in this chapter will not run in the simulator because the simulator does not support Bluetooth. The only way to build and debug apps on a device attached to your machine is by joining the paid iPhone Developer Program. So you'll need to do that if you want to fully experience this chapter's chewy goodness.

In addition, the game we're building in this chapter requires the use of two second-generation devices (iPhone 3G or 3Gs, or second-generation iPod touch) to run and test. As of this writing, you cannot play GameKit games between a device and the simulator. If you have only one device, you will not be able to try out the game in this chapter. We will be adding online play in the next chapter, so you might want to follow along, even if you can't test your application yet.

As of this writing, GameKit has three basic components:

  • The session allows iPhone OS devices running the same application to easily send information back and forth over Bluetooth without writing any networking code.

  • The peer picker provides an easy way to find other devices without writing any networking or discovery (Bonjour) code.

  • The in-game voice functionality allows users to send voice communications using GameKit sessions or over the Internet.

Note

We won't use in-game voice in this chapter's example, but it's actually pretty straightforward. If you want to learn more about it, here's a link to the official Apple doc:

http://developer.apple.com/iPhone/library/documentation/NetworkingInternet/Conceptual/GameKit_Guide/InGameVoice/InGameVoice.html

Under the hood, GameKit sessions leverage Bonjour, Apple's technology for zero-configuration network device discovery. As a result, devices using GameKit are capable of finding each other on the network without the user needing to enter an IP address or domain name.

This Chapter's Application

In this chapter, we're going to explore GameKit by writing a simple networked game. We'll write a two-player version of tic-tac-toe (Figure 8-1) that will use GameKit to let people on two different iPhones or iPod touches play against each other over Bluetooth. We won't be implementing online play over the Internet or local area network in this chapter. However, we will discuss online communications in the next chapter.

We'll use a simple game of tic-tac-toe to show you the basics of GameKit.

Figure 8.1. We'll use a simple game of tic-tac-toe to show you the basics of GameKit.

When users launch our application, they will be presented with an empty tic-tac-toe board and a single button labeled New Game. (For the sake of simplicity, we're not going to implement a single-device mode to let two players play on the same device.) When the user presses the New Game button, the application will start looking for Bluetooth peers using the peer picker (Figure 8-2).

When the user presses the New Game button, it will launch the peer picker to look for other devices running the tic-tac-toe game.

Figure 8.2. When the user presses the New Game button, it will launch the peer picker to look for other devices running the tic-tac-toe game.

If another device within range runs the TicTacToe application, and the user also presses the New Game button, then the two devices will find each other, and the peer picker will present a dialog to the users, letting them choose among the available peers (Figure 8-3).

When another device within range starts a game, the two devices will show up in each other's peer picker dialog.

Figure 8.3. When another device within range starts a game, the two devices will show up in each other's peer picker dialog.

After one player selects a peer, the other person will be asked to accept or refuse the connection. If the connection is accepted, the two applications will negotiate to see who goes first. Each side will randomly select a number, the numbers will be compared, and the highest number will go first. Once that decision is made, play will commence (Figure 8-4) until someone wins (Figure 8-5).

The user whose turn it is can tap any available space. That space will get an X or an O on both users' devices.

Figure 8.4. The user whose turn it is can tap any available space. That space will get an X or an O on both users' devices.

Play continues until one player wins or there is a tie.

Figure 8.5. Play continues until one player wins or there is a tie.

Network Communication Models

Before we look at how GameKit and the peer picker work, let's talk generally about communication models used in networked programs, so that we're all on the same page in terms of terminology.

Client-Server Model

You're probably familiar with the client-server model, as it is the model used by the World Wide Web. Machines called servers listen for connections from other machines, referred to as clients. The server then takes actions based on the requests received from the clients. In the context of the Web, the client is usually a web browser, and there can be any number of clients attaching to a single server. The clients never communicate with each other directly, but direct all communications through the server. Most massively multiplayer online role-playing games (MMORPGs) like World of Warcraft also use this model. Figure 8-6 represents a client-server scenario.

The client-server model features one machine acting as a server with all communications—even communications between clients—going through the server.

Figure 8.6. The client-server model features one machine acting as a server with all communications—even communications between clients—going through the server.

In the context of an iPhone application, a client-server setup is where one phone acts as a server and listens for other iPhones running the same program. The other phones can then connect to that server. If you've ever played a game where one machine "hosts" a game and others then join the game, that game is almost certainly using a client-server model.

A drawback with the client-server model is that everything depends on the server, which means that the game cannot continue if anything happens to the server. If the user whose phone is acting as the server quits, crashes, or moves out of range, the entire game is ended. Since all the other machines communicate through the central server, they lose the ability to communicate if the server is unavailable. This is generally not an issue with client-server games where the client is a hefty server farm connected to the Internet by redundant high-speed lines, but it certainly can be an issue with mobile games.

Peer-to-Peer Model

In the peer-to-peer model, all the individual devices (called peers) can communicate with each other directly. A central server may be used to initiate the connection or to facilitate certain operations, but the main distinguishing feature of the peer-to-peer model is that peers can talk to each other directly, and can continue to do so even in the absence of a server (Figure 8-7).

The peer-to-peer model was popularized by file-sharing services like BitTorrent. A centralized sever is used to find other peers that have the file you are looking for, but once the connection is made to those other peers, they can continue, even if the server goes offline.

In the peer-to-peer model, peers can talk to each other directly, and can continue to do so even in the absence of a server.

Figure 8.7. In the peer-to-peer model, peers can talk to each other directly, and can continue to do so even in the absence of a server.

The simplest and probably the most common implementation of the peer-to-peer model on the iPhone is when you have two devices connected to each other. This is the model you use in head-to-head games, for example. GameKit makes this kind of peer-to-peer network exceedingly simple to set up and configure, as you'll see in this chapter.

Hybrid Client-Server/Peer-to-Peer

The client-server and peer-to-peer models of network communication are not mutually exclusive, and it is possible to create programs that utilize a hybrid of both. For example, a client-server game might allow certain communications to go directly from client to client, without going through the server. In a game that had a chat window, it might allow messages intended for only one recipient to go directly from the machine of the sender to the machine of the intended recipient, while any other kind of chat would go to the server to be distributed to all clients.

You should keep these different networking models in mind as we discuss the mechanics of making connections and transferring data between application nodes. Node is a generic term that refers to any computer connected to an application's network. A client, server, or peer is a node. The game we will be writing in this chapter will use a simple, two-machine, peer-to-peer model.

The GameKit Session

The key to GameKit is the session, represented by the class GKSession. The session represents our end of a network connection with one or more other iPhones. Regardless of whether you are acting as a client, a server, or a peer, an instance of GKSession will represent the connections you have with other phones. You will use GKSession whether you employ the peer picker or write your own code to find machines to connect to and let the user select from them.

Note

As you make your way through the next few pages, don't worry too much about where each of these elements is implemented. This will all come together in the project you create in this chapter.

You will also use GKSession to send data to connected peers. You will implement session delegate methods to get notified of changes to the session, such as when another node connects or disconnects, as well as to receive data sent by other nodes.

Creating the Session

To use a session, you must first create allocate and initialize a GKSession object, like so:

GKSession *theSession = [[GKSession alloc] initWithSessionID:@"com.apress.Foo"
    displayName:nil sessionMode:GKSessionModePeer];

There are three arguments you pass in when initializing a session:

  • The first argument is a session identifier, which is a string that is unique to your application. This is used to prevent your application's sessions from accidentally connecting to sessions from another program. Since the session identifier is a string, it can be anything, though the convention is to use a reverse DNS-style name, such as com.apress.Foo. By assigning session identifiers in this manner, rather than by just randomly picking a word or phrase, you are less likely to accidentally choose a session identifier that is used by another application on the App Store.

  • The second argument is the display name. This is a name that will be provided to the other nodes to uniquely identify your phone. If you pass in nil, the display name will default to the device's name as set in iTunes. If multiple devices are connected, this will allow the other users to see which devices are available and connect to the correct one. In Figure 8-3, you can see an example of where the unique identifier is used. In that example, one other device is advertising itself with the same session identifier as us, using a display name of iPhone.

  • The last argument is the session mode. Session modes determine how the session will behave once it's all set up and ready to make connections. There are three options:

    • If you specify GKSessionModeServer, your session will advertise itself on the network so that other devices can see it and connect to it, but it won't look for other sessions being advertised.

    • If you specify GKSessionModeClient, the session will not advertise itself on the network, but will look for other sessions that are advertising themselves.

    • If you specify GKSessionModePeer, your session will both advertise its availability on the network and also look for other sessions.

Note

Although you will generally use GKSessionModePeer when establishing a peer-to-peer network, and GKSessionModeServer and GKSessionModeClient when setting up a client-server network, these constants dictate only whether an individual session will advertise its availability on the network using Bonjour, or look for other available nodes. They are not necessarily indicative of which of the network models is being used by the application.

Regardless of the type of session you create, it won't actually start advertising its availability or looking for other available nodes until you tell it to do so. You do that by setting the session property available to YES. Alternatively, you can have the node stop advertising its availability and/or stop looking for other available nodes by setting available to NO.

Finding and Connecting to Other Sessions

When a session that was created using GKSessionModeClient or GKSessionModePeer finds another node advertising its availability, it will call the method session:peer:didChangeState: and pass in a state of GKPeerStateAvailable. This same method will be called every time a peer becomes available or unavailable, as well as when a peer connects or disconnects. The second argument will tell you which peer's state changed, and the last argument will tell you its new state.

If you find one or more other sessions that are available, you can choose to connect the session to one of the available sessions by calling connectToPeer:withTimeout:. Here's an example of session:peer:didChangeState: that connects to the first available peer it finds:

- (void)session:(GKSession *)session peer:(NSString *)peerID
        didChangeState:(GKPeerConnectionState)inState {
    if (inState == GKPeerStateAvailable) {
        [session connectToPeer:peerID withTimeout:60];
        session.available = NO;
    }
}

This isn't a very realistic example, as you would normally allow the user to choose the node to which they connect. It's a good example though, because it shows both of the basic functions of a client node. In this example, we've set available to NO after we connect. This will cause our session to stop looking for additional sessions. Since a session can connect to multiple peers, you won't always want to do this. If your application supports multiple connections, then you will want to leave it at YES.

Listening for Other Sessions

When a session is specified with a session mode of GKSessionModeServer or GKSessionModePeer, it will be notified when another node attempts to connect. When this happens, the session will call the method session:didReceiveConnectionRequestFromPeer:. You can choose to accept the connection by calling acceptConnectionFromPeer:error:, or you can reject it by calling denyConnectionFromPeer:. The following is an example that assumes the presence of a Boolean instance variable called amAcceptingConnections. If it's set to YES, it accepts the connection, and if it's set to NO, it rejects the connection.

- (void)session:(GKSession *)session
didReceiveConnectionRequestFromPeer:(NSString *)peerID {
    if (amAcceptingConnections) {
        NSError *error;
        if (![session acceptConnectionFromPeer:peerID error:&error])
            // Handle error
    } else {
        [session denyConnectionFromPeer:peerID];
    }
}

Sending Data to a Peer

Once you have a session that is connected to another node, it's very easy to send data to that node. All you need to do is call one of two methods. Which method you call depends on whether you want to send the information to all connected sessions or to just specific ones. To send data to just specified peers, you use the method sendData:toPeers:withDataMode:error:, and to send data to every connected peer, you use the method sendDataToAllPeers:withDataMode:error:.

In both cases, you need to specify a data mode for the connection. The data mode tells the session how it should try to send the data. There are two options:

  • GKSendDataReliable: This option ensures that the information will arrive at the other session. It will send the data in chunks if it's over a certain size, and wait for an acknowledgment from the other peer for every chunk.

  • GKSendDataUnreliable: This mode sends the data immediately and does not wait for acknowledgment. It's much faster than GKSendDataReliable, but there is a small chance of the complete message not arriving at the other node.

Usually, the GKSendDataReliable data mode is the one you'll want to use, though if you have a program where speed of transmission matters more than accuracy, then you'll want to consider GKSendDataUnreliable.

Here is what it looks like when you send data to a single peer:

NSError *error = nil;
    If (![session sendData:theData toPeers:[NSArray arrayWithObject:thePeerID]
        withDataMode:GKSendDataReliable error:&error]) {
        // Do error handling
    }

And here's what it looks like to send data to all connected peers:

NSError *error = nil;
    if (![session sendDataToAllPeers:data withDataMode:GKSendDataReliable
        error:&error]) {
        // Do error handling
    }

Packaging Up Information to Send

Any information that you can get into an instance of NSData can be sent to other peers. There are two basic approaches to doing this for use in GameKit. The first is to use archiving and unarchiving, just as we did in the archiving section of Chapter 11 of Beginning iPhone 3 Development (Apress, 2009).

With the archiving/unarchiving method, you define a class to hold a single packet of data to be sent. That class will contain instance variables to hold whatever types of data you might need to send. When it's time to send a packet, you create and initialize an instance of the packet object, and then you use NSKeyedArchiver to archive the instance of that object into an instance of NSData, which can be passed to sendData:toPeers:withDataMode:error: or to sendDataToAllPeers:withDataMode:error:. We'll use this approach in this chapter's example. However, this approach incurs a small amount of overhead, since it requires the creation of objects to be passed, along with archiving and unarchiving those objects.

Although archiving objects is the best approach in many cases, because it is easy to implement and it fits well with the design of Cocoa Touch, there may be some cases where applications need to constantly send a lot of data to their peers, and this overhead might be unacceptable. In those situations, a faster option is to just use a static array (a regular old C array, not an NSArray) as a local variable in the method that sends the data.

You can copy any data you need to send to the peer into this static array, and then create an NSData instance from that static array. There's still some object creation involved in creating the NSData instance, but it's one object instead of two, and you don't have the overhead of archiving. Here's a simple example of sending data using this faster technique:

NSUInteger packetData[2];
    packet[0] = foo;
    packet[1] = bar;
    NSData *packet = [NSData dataWithBytes:packetData
        length:2 * sizeof(packetData)];
    NSError *error = nil;
    if (![session sendDataToAllPeers:packet withDataMode:GKSendDataReliable
        error:&error]) {
        // Handle error
    }

Receiving Data from a Peer

When a session receives data from a peer, the session passes the data to a method on an object known as a data receive handler. The method is receiveData:fromPeer:inSession:context:. By default, the data receive handler is the session's delegate, but it doesn't have to be. You can specify another object to handle the task by calling setDataReceiveHandler:withContext: on the session and passing in the object you want to receive data from the session.

Whichever object is specified as the data receive handler must implement receiveData:fromPeer:inSession:context:, and that method will be called any time new data comes in from a peer. There's no need to acknowledge receipt of the data or worry about waiting for the entire packet. You can just use the provided data as is appropriate for your program. All the gnarly aspects of network data transmission are handled for you. Every call to sendDataToAllPeers:withDataMode:error: made by other peers, and every call to sendData:toPeers:withDataMode:error: made by other peers who specify your peer identifier, will result in one call of the data receive handler.

Here's an example of a data receive handler method that would be the counterpart to our earlier send example:

- (void) receiveData:(NSData *)data fromPeer:(NSString *)peer
inSession: (GKSession *)theSession context:(void *)context {
    NSUInteger *packet = [data bytes];
    NSUInteger foo = packet[0];
    NSUInteger bar = packet[0];
    // Do something with foo and bar
}

We'll look at receiving archived objects when we build this chapter's example.

Closing Connections

When you're finished with a session, before you release the session object, it's important to do a little cleanup. Before releasing the session object, you must make the session unavailable, disconnect it from all of its peers, set the data receive handler to nil, and set the session delegate to nil. Here's what the code in your dealloc method (or any other time your need to close the connections) might look like:

session.available = NO;
    [session disconnectFromAllPeers];

    [session setDataReceiveHandler: nil withContext: nil];
    session.delegate = nil;
    [session release];

If, instead, you just want to disconnect from one specific peer, you can call disconnectPeerFromAllPeers:, which will disconnect the remote peer from all the peers to which it was connected. Use this method with caution, as it will cause the peer on which it was called to disconnect from all remote peers, not just your application. Here's what using it might look like:

[session disconnectPeerFromAllPeers:thePeer];

The Peer Picker

Although GameKit does not need to be used only for games, network games are clearly the primary motivator behind the technology—at least if the name Apple chose is any clue. The most common type of network model for mobile games is the head-to-head or simple peer-to-peer model, where one player plays a game against one other player. Because this scenario is so common, Apple has provided a mechanism called the peer picker for easily setting up this simple type of peer-to-peer network.

Creating the Peer Picker

The peer picker was designed specifically to connect one device to a single other device using Bluetooth. Though limited in this way, the peer picker is incredibly simple to use, and a great choice if it meets your needs. To create and show the peer picker, you just create an instance of GKPeerPickerController, set its delegate, and then call its show method, like so:

GKPeerPickerController *picker;
    picker = [[GKPeerPickerController alloc] init];
    picker.delegate = self;
    [picker show];

One important thing to note here is that it looks like we're leaking the picker here (we've used alloc with no corresponding release), but that's not the case. This is one of those unusual exceptions to the general rule. The reason it's okay to leak the memory is that the delegate (which is the object where the preceding code appears, since it's set to self) will be called again when the user is finished interacting with the peer picker. The delegate method will be passed back a reference to the same peer picker controller instance that was leaked here. At that point, the delegate can release the peer picker, and no memory will have been leaked during the filming of this application.

Handling a Peer Connection

When the user has selected a peer and the sessions have been connected to each other, the delegate method peerPickerController:didConnectToPeer:toSession: will be called. In your implementation of that method, you need to do a few things. First, you might want to store the peer identifier, which is a string that identifies the device to which you're connected. The peer identifier defaults to the iPhone's device name, though you can specify other values. You also need to save a reference to the session so you can use it to send data and to disconnect the session later. Additionally, it's important to dismiss the peer picker and make sure that its memory is not leaked. Remember that you didn't retain it when you created it, so you are responsible for releasing it here.

We use autorelease, instead of release, to give the calling object (which is, in fact, picker) the ability to finish the method that's currently executing—the one that called this delegate method. If we were to use release, the object could (and probably would) be released immediately, which would mean the calling method would never finish, and the connection might not finish being established. By putting picker into the autorelease pool, we ensure that it won't be deallocated until the end of the current run loop, so it will have the opportunity to finish any work it's in the process of doing, yet we'll still avoid leaking memory. It is still true that you should avoid unnecessary use of the autorelease pool, but here it isn't unnecessary.

- (void)peerPickerController:(GKPeerPickerController *)picker
didConnectPeer:(NSString *)thePeerID
toSession:(GKSession *)theSession {
    self.peerID = thePeerID;

    self.session = theSession;
    self.session.delegate = self;
    [self.session setDataReceiveHandler:self withContext:NULL];

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

Creating the Session

There's one last delegate task that you must handle when using the peer picker, which is to create the session when the picker asks for a session. You don't need to worry about most of the other tasks related to finding and connecting to other peers when using the peer picker, but you are responsible for creating the session for the picker to use. Here's what that method typically looks like:

- (GKSession *)peerPickerController:(GKPeerPickerController *)picker
sessionForConnectionType:(GKPeerPickerConnectionType)type{
    GKSession *theSession = [[GKSession alloc] initWithSessionID:@"a session id"
        displayName:nil sessionMode:GKSessionModePeer];
    return [theSession autorelease];
}

We've already talked about the session, so there shouldn't be anything in this method that's confusing.

Note

There's actually another peer picker delegate method that you need to implement if you want to support online play over the Internet with the peer picker: peerPickerController:didSelectConnectionType:. We'll look at that method in the next chapter.

Well, that's enough discussion. Let's start building our application.

Creating the Project

Okay, you know the drill. Fire up Xcode if it's not already open and create a new project. Use the View-based Application template and call the project TicTacToe. Once the project is open, look in the project archives that accompany this book, in the folder 08 – TicTacToe. Find the image files called wood_button.png, board.png, O.png, and X.png, and copy them into the Resources folder of your project. There's also an icon file called icon.png, which you can copy into your project if you want to use it.

Turning Off the Idle Timer

The first thing we want to do is to turn off the idle timer. The idle timer is what tells your iPhone to go to sleep if the user has not interacted with it in a while. Because the user won't be tapping the screen during the opponent's turn, we need to turn this off to prevent the phone from going to sleep if the other user takes a while to make a move. Generally speaking, you don't want networked applications to go to sleep, because sleeping breaks the network connection. Most of the time, with networked iPhone games, disabling the idle timer is the best approach.

Expand the Classes folder in the Groups & Files pane in Xcode and single-click TicTacToeAppDelegate.m. Add the following line of code to applicationDidFinishLaunching: to disable the idle timer.

- (void)applicationDidFinishLaunching:(UIApplication *)application {
    // Override point for customization after app launch
    [window addSubview:viewController.view];
    [window makeKeyAndVisible];

    [[UIApplication sharedApplication] setIdleTimerDisabled:YES];
}

Note

There may be rare times when you want to leave the idle timer functioning and just close your sessions when the app goes to sleep, but closing sessions on sleep is not quite as straightforward as it would seem. The application delegate method applicationWillResignActive: is called before the phone goes to sleep, but unfortunately, it's also called at other times. In fact, it's called any time that your application loses the ability to respond to touch events. That makes it close to impossible to differentiate between when the user has been presented a system alert, such as from a push notification or a low-battery warning (which won't result in broken connections), and when the phone is actually going to sleep. So, until Apple provides a way to differentiate between these scenarios, your best bet is to simply disallow sleep while a networked program is running.

Importing the GameKit Framework

GameKit is not one of the frameworks that is automatically linked by the Xcode project template, so we need to manually link it ourselves in order to access the session and peer picker methods. Select the Frameworks folder in the Groups & Files pane. Now, right-click the Frameworks folder and select Add from the context menu, and then choose Existing Frameworks....

If you're using Xcode 3.2 or higher (which requires Snow Leopard), you'll notice that there's a new, easier way to select frameworks (Figure 8-8). You can just select GameKit.framework from a provided list of frameworks and then hit the Add button. If you're still running Leopard or an earlier version of Xcode, you'll need to link the old-fashioned way: by navigating through the file system to the Frameworks folder for the version of the iPhone SDK that you're using, and then selecting GameKit.framework. The Frameworks folder is at the following location:

/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOSx.y.z.sdk/System/Library/Frameworks

In this path, x, y, and z denote the release number. For iPhone SDK 3.1.2, for example (the current version as of this writing), x is 3, y is 1, and z is 2. In that case, you would need to navigate to this location:

/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS3.1.2.sdk/System/Library/Frameworks
The new Snow Leopardy way of linking frameworks.

Figure 8.8. The new Snow Leopardy way of linking frameworks.

Designing the Interface

Now, we're going to design our game's user interface. Since tic-tac-toe is a relatively simple game, we'll design our user interface in Interface Builder, rather than by using OpenGL ES.

Each of the spaces on the board will be a button. When the user taps a button that hasn't already been selected (which we'll determine by seeing if the button has an image assigned), we'll set the image to either X.png or O.png (which you added to your project a few minutes ago). We'll then send that information to the other device. We're also going to use the button's tag value to differentiate the buttons and make it easier to determine when someone has won. We'll assign each of the buttons that represents a space on the board with a sequential tag, starting in the upper-left corner. You can see which space will have which tag value by looking at Figure 8-9.

We will assign each of the game space buttons a tag value. This way, we can identify which button was pressed without needing to have separate action methods for each button.

Figure 8.9. We will assign each of the game space buttons a tag value. This way, we can identify which button was pressed without needing to have separate action methods for each button.

Setting Up the View Controller Header

Before we head over to Interface Builder to actually create our user interface, we want to declare the actions and outlets that we'll need to connect once we get there. While we're in the header file, we'll also declare the rest of the methods we'll be using, as well as some constants and enumerations to make our code easier to read.

Single-click TicTacToeViewController.h and make the following changes:

#import <UIKit/UIKit.h>
#import <GameKit/GameKit.h>

#define kTicTacToeSessionID     @"com.apress.TicTacToe.session"
#define kTicTacToeArchiveKey    @"com.apress.TicTacToe"

typedef enum GameStates {
kGameStateBeginning,
    kGameStateRollingDice,
    kGameStateMyTurn,
    kGameStateOpponentTurn,
    kGameStateInterrupted,
    kGameStateDone
} GameState;

typedef enum BoardSpaces {
    kUpperLeft = 1000, kUpperMiddle, kUpperRight,
    kMiddleLeft, kMiddleMiddle, kMiddleRight,
    kLowerLeft, kLowerMiddle, kLowerRight
} BoardSpace;

typedef enum PlayerPieces {
    kPlayerPieceUndecided,
    kPlayerPieceO,
    kPlayerPieceX
} PlayerPiece;

@class TicTacToePacket;
@interface TicTacToeViewController : UIViewController
@interface TicTacToeViewController : UIViewController <GKPeerPickerControllerDelegate,
GKSessionDelegate, UIAlertViewDelegate> {
    UIButton    *newGameButton;
    UILabel     *feedbackLabel;

    GKSession   *session;
    NSString    *peerID;

    GameState   state;

    NSInteger   myDieRoll;
    NSInteger   opponentDieRoll;

    PlayerPiece piece;
    UIImage     *xPieceImage;
    UIImage     *oPieceImage;

    BOOL        dieRollReceived;
    BOOL        dieRollAcknowledged;

}
@property(nonatomic, retain) IBOutlet UIButton *newGameButton;
@property(nonatomic, retain) IBOutlet UILabel *feedbackLabel;

@property(nonatomic, retain) GKSession    *session;
@property(nonatomic, copy)   NSString     *peerID;

@property(nonatomic, retain) UIImage *xPieceImage;
@property(nonatomic, retain) UIImage *oPieceImage;

- (IBAction)newGameButtonPressed;
- (IBAction)gameSpacePressed:(id)sender;
- (void)resetBoard;
- (void)startNewGame;
- (void)resetDieState;
- (void)sendPacket:(TicTacToePacket *)packet;
- (void)sendDieRoll;
- (void)checkForGameEnd;
@end

The first thing we need to do is import the GameKit headers so that the compiler knows about the objects and methods from GameKit:

#import <GameKit/GameKit.h>

Next, we define two constants. One will be our session identifier, which GameKit will use to make sure we connect only to devices running the same program. The other is an archiving key that we will use when packaging data to send to the other node.

#define kTicTacToeSessionID     @"com.apress.TicTacToe.session"
#define kTicTacToeArchiveKey    @"com.apress.TicTacToe"

Adding networking to even a simple application creates a fair bit of complexity, because network communications are asynchronous. You can't make any assumptions about the order that data will be received. To help us keep track of where we are, we define an enum with a bunch of different game states that identify what's going on right now in our game:

typedef enum GameStates {
    kGameStateBeginning,
    kGameStateRollingDice,
    kGameStateMyTurn,
    kGameStateOpponentTurn,
    kGameStateInterrupted,
    kGameStateDone
} GameState;

When a game has not yet begun, the state will be kGameStateBeginning. After the devices connect, or when a new game is started, the two nodes will negotiate who goes first by each generating a random number, which is equivalent to flipping a coin or rolling a die in real life. When who goes first is being negotiated, the state is kGameStateRollingDice. When it's our turn to make a move, the state will be kGameStateMyTurn, and when it's the opponent's turn, the state will be kGameStateOpponentTurn. If the connection is interrupted for any reason, we'll set the state to kGameStateInterrupted. Finally, if there are no more possible moves or a player gets three in a row, the state will move to kGameStateDone.

Next, we define another enumeration to refer to each of the spaces on the board by their tag:

typedef enum BoardSpaces {
    kUpperLeft = 1000, kUpperMiddle, kUpperRight,
    kMiddleLeft, kMiddleMiddle, kMiddleRight,
    kLowerLeft, kLowerMiddle, kLowerRight
} BoardSpace;

In tic-tac-toe, the player who goes first is O, and the other player is X. To identify who is X and who is O, we have one more enumeration:

typedef enum PlayerPieces {
    kPlayerPieceUndecided,
    kPlayerPieceO,
    kPlayerPieceX
} PlayerPiece;

After that, we tell the compiler that there is a class called TicTacToePacket. This class doesn't exist yet, but we'll write it shortly. A @class declaration doesn't cause the compiler to look for the class' header file—it's just a promise that a class really exists, so it's okay to declare it this way before actually creating or writing the class.

@class TicTacToePacket;

Our controller class needs to conform to a few protocols. Our controller will be the delegate of the peer picker, the session. We'll also be using alert views to inform the user when there's a problem, so we conform our class to the three protocols used to define the delegate methods for each of these jobs.

@interface TicTacToeViewController : UIViewController <GKPeerPickerControllerDelegate, GKSessionDelegate, UIAlertViewDelegate> {

If you look at the interface in Figures 8-1 and 8-4, you can see that there's a button for starting a game, as well as a label that's used to tell users if it's their turn, or if they've won or lost. We need instance variables for outlets to both of those:

UIButton    *newGameButton;
    UILabel     *feedbackLabel;

We also need instance variables for the GameKit session and to hold the peer identifier of the one connected node.

GKSession   *session;
    NSString    *peerID;

A moment ago, we defined an enumeration with the various game states, but we need an instance variable to keep track of the current state:

GameState   state;

Because we don't know whether we will roll the die or receive our opponent's die roll first, we need variables to hold them both. Once we have both, we can compare them and start the game:

NSInteger   myDieRoll;
    NSInteger   opponentDieRoll;

Once we know who goes first, we can store whether we're O or X in this instance variable:

PlayerPiece piece;

We'll also load both of the images representing the two game pieces when our view is loaded, and keep a reference to them in these pointers:

UIImage     *xPieceImage;
UIImage     *oPieceImage;

Finally, we have two more Booleans to keep track of whether we've received the opponent's die roll and whether our opponent has acknowledged receipt of ours. We don't want to begin the game until we have both die rolls and we know our opponent has both as well. When both of these are YES, we'll know it's time to start the actual game play:

BOOL        dieRollReceived;
    BOOL        dieRollAcknowledged;
}

Next, we define properties for our outlets, as well as some of our instance variables:

@property(nonatomic, retain) IBOutlet UIButton *newGameButton;
@property(nonatomic, retain) IBOutlet UILabel *feedbackLabel;

@property(nonatomic, retain) GKSession *session;
@property(nonatomic, copy)   NSString  *peerID;

@property(nonatomic, retain) UIImage *xPieceImage;
@property(nonatomic, retain) UIImage *oPieceImage;

And, finally, we declare our action methods and a bunch of other methods that we'll need in our game. We'll discuss the specific methods in more detail when we implement our controller later, after we design our user interface.

- (IBAction)newGameButtonPressed;
- (IBAction)gameSpacePressed:(id)sender;
- (void)resetBoard;
- (void)startNewGame;
- (void)resetDieState;
- (void)sendPacket:(TicTacToePacket *)packet;
- (void)sendDieRoll;
- (void)checkForGameEnd;
@end

Save this file.

Now, expand the Resources folder in the Groups & Files pane, if it's not already expanded, and double-click TicTacToeViewController.xib to open Interface Builder.

Designing the Game Board

Once Interface Builder is open, look in the library for an Image View and drag that to the window labeled View. Because it's the first object you're adding to the view, it should resize to take up the full view. Place it so that it fills the entire view, and then press

Designing the Game Board

Next, drag a Round Rect Button from the library over to the top of the view. The exact placement doesn't matter yet. After it's placed, use the attribute inspector to change the button type from Rounded Rect to Custom. In the Image field of the attribute inspector, select wood_button.png, and then press

Designing the Game Board
Your interface after sizing and placing the button

Figure 8.10. Your interface after sizing and placing the button

Control-drag from File's Owner to the button and select the newGameButton outlet. Then Control-drag from the button back to File's Owner, and select the newGameButtonPressed action.

Look again in the library for a Label, and drag it to the view. Place the label in the top of the view so it runs from the left blue margin to the right blue margin horizontally, and from the top blue margin down to just above the tic-tac-toe board. It will overlap the button we just added, and that's okay, because the label will display text only when the button isn't visible. Use the attribute inspector to center the text, and the font palette (

Your interface after sizing and placing the button

Control-drag from File's Owner to the new label and select the feedbackLabel outlet.

Now, we need to add a button for each of the nine game spaces and assign them each a tag value so that our code will have a way to identify which space on the board each button represents. Drag nine Round Rect Buttons to the view, and use the attribute inspector to change their type to Custom. Use the size inspector to place them in the locations specified in Table 8-1, and use the attribute inspector to assign them the listed tag value. Here's one shortcut to consider: Create one, set its size and attributes, and then start making copies.

Table 8.1. Game Space Locations, Sizes, and Tags

Game Space

X

Y

Width

Height

Tag

Upper Left

24

122

86

98

1000

Upper Middle

120

122

86

98

1001

Upper Right

217

122

86

98

1002

Middle Left

24

230

86

98

1003

Middle

120

230

86

98

1004

Middle Right

217

230

86

98

1005

Lower Left

24

336

86

98

1006

Lower Middle

120

336

86

98

1007

Lower Right

217

336

86

98

1008

Once you have the buttons in place, Control-drag from each of the nine buttons to File's Owner and select the gameSpacePressed: action.

Finally, save the nib, and then quit Interface Builder.

Creating the TicTacToePacket Object

Once you're back in Xcode, single-click the Classes folder in the Groups & Files pane, and select New File... from the File menu. Select the Objective-C class template, with NSObject selected for the Subclass of pop-up menu. We're going to create the class that will be used to send information back and forth between the two nodes, so name this file TicTacToePacket.m and make sure the Also create "TicTacToePacket.h" check box is checked.

Once the files are created, single-click TicTacToePacket.h and replace its contents with the following:

#import <Foundation/Foundation.h>
#import "TicTacToeViewController.h"

#define dieRoll() (arc4random() % 1000000)
#define kDiceNotRolled  INT_MAX

typedef enum PacketTypes {
    kPacketTypeDieRoll,         // used to determine who goes first
    kPacketTypeAck,             // used to acknowledge die roll packet receipt
    kPacketTypeMove,            // used to send information about a player's move
    kPacketTypeReset,           // used to inform the peer that we're starting over
} PacketType;
@interface TicTacToePacket : NSObject <NSCoding> {
    PacketType  type;
    NSUInteger  dieRoll;
    BoardSpace  space;
}
@property PacketType type;
@property NSUInteger dieRoll;
@property BoardSpace space;
- (id)initWithType:(PacketType)inType
    dieRoll:(NSUInteger)inDieRoll
    space:(BoardSpace)inSpace;
- (id)initDieRollPacket;
- (id)initDieRollPacketWithRoll:(NSUInteger)inDieRoll;
- (id)initMovePacketWithSpace:(BoardSpace)inSpace;
- (id)initAckPacketWithDieRoll:(NSUInteger)inDieRoll;
- (id)initResetPacket;
@end

Much of this code should be fairly intuitive. We define a macro for generating a random number to resolve who goes first. It generates a number between 0 and 999,999. We're using a large number here so that the chance of both devices rolling the same value (which would require a reroll) will be extremely low. We also define a constant that will identify when the die has not yet been rolled. Remember that we're storing both our die roll and our opponent's die roll in NSInteger instance variables. On the iPhone, NSInteger is the same as an int. We use the value INT_MAX to identify when those values have not yet been determined. INT_MAX is the largest value that an int can hold on the platform. Since the largest number our dieRoll() macro will generate is 999,999, we can safely use INT_MAX to identify when a die hasn't been rolled, because INT_MAX equals 2,147,483,647 on current iPhones. If INT_MAX ever changes, it will likely get bigger, not smaller.

#define dieRoll() (arc4random() % 1000000)
#define kDiceNotRolled  INT_MAX

We define an enum with each of the different types of packets we'll need to send to the other node:

typedef enum PacketTypes {
    kPacketTypeDieRoll,         // used to determine who goes first
    kPacketTypeAck,             // used to acknowledge die roll packet receipt
    kPacketTypeMove,            // used to send information about a player's move
    kPacketTypeReset,           // used to inform the peer that we're starting over
} PacketType;

In our class definition, we have only three instance variables: one to identify the type of packet and two others to hold information that might need to be sent as part of that packet. The only other pieces of information we ever need to send are the results of a die roll and which space on the game board a player placed an X or O. We also conform our class to the NSCoding protocol so that we can archive it into an NSData instance to send through the GameKit session:

@interface TicTacToePacket : NSObject <NSCoding> {
    PacketType  type;
    NSUInteger  dieRoll;
BoardSpace  space;
}

We expose all three instance variables using properties:

@property PacketType type;
@property NSUInteger dieRoll;
@property BoardSpace space;

And we declare a handful of init methods for creating the different types of packets we will send:

- (id)initWithType:(PacketType)inType
    dieRoll:(NSUInteger)inDieRoll
    space:(BoardSpace)inSpace;
- (id)initDieRollPacket;
- (id)initDieRollPacketWithRoll:(NSUInteger)inDieRoll;
- (id)initMovePacketWithSpace:(BoardSpace)inSpace;
- (id)initAckPacketWithDieRoll:(NSUInteger)inDieRoll;
- (id)initResetPacket;
@end

Save TicTacToePacket.h.

Next, switch over to TicTacToePacket.m and replace the contents of the file with this new version:

#import "TicTacToePacket.h"

@implementation TicTacToePacket
@synthesize type;
@synthesize dieRoll;
@synthesize space;

#pragma mark -
- (id)initWithType:(PacketType)inType dieRoll:(NSUInteger)inDieRoll space:(BoardSpace)inSpace {
   if (self = [super init]) {
       type = inType;
       dieRoll = inDieRoll;
       space = inSpace;
   }
    return self;
}

- (id)initDieRollPacket {
    int roll = dieRoll();
    return [self initWithType:kPacketTypeDieRoll dieRoll:roll space:0];
}

- (id)initDieRollPacketWithRoll:(NSUInteger)inDieRoll {
    return [self initWithType:kPacketTypeDieRoll dieRoll:inDieRoll space:0];
}

- (id)initMovePacketWithSpace:(BoardSpace)inSpace{
    return [self initWithType:kPacketTypeMove dieRoll:0 space:inSpace];
}

- (id)initAckPacketWithDieRoll:(NSUInteger)inDieRoll {
return [self initWithType:kPacketTypeAck dieRoll:inDieRoll space:0];
}

- (id)initResetPacket {
    return [self initWithType:kPacketTypeReset dieRoll:0 space:0];
}

#pragma mark -
- (NSString *)description {
    NSString *typeString = nil;
    switch (type) {
        case kPacketTypeDieRoll:
            typeString = @"Die Roll";
            break;
        case kPacketTypeMove:
            typeString = @"Move";
            break;
        case kPacketTypeAck:
            typeString = @"Ack";
            break;
        case kPacketTypeReset:
            typeString = @"Reset";
        default:
            break;
    }
    return [NSString stringWithFormat:@"%@ (dieRoll: %d / space: %d)", typeString,
        dieRoll, space];
}

#pragma mark -
#pragma mark NSCoder (Archiving)
- (void)encodeWithCoder:(NSCoder *)coder {
    [coder encodeInt:[self type] forKey:@"type"];
    [coder encodeInteger:[self dieRoll] forKey:@"dieRoll"];
    [coder encodeInt:[self space] forKey:@"space"];
}

- (id)initWithCoder:(NSCoder *)coder  {
    if (self = [super init]) {
        [self setType:[coder decodeIntForKey:@"type"]];
        [self setDieRoll:[coder decodeIntegerForKey:@"dieRoll"]];
        [self setSpace:[coder decodeIntForKey:@"space"]];
    }
    return self;
}

@end

TicTacToePacket is a fairly straightforward class. There shouldn't be anything in its implementation that you haven't seen before. Save TicTacToePacket.m. Next, we'll write our view controller and finish up our application.

Implementing the Tic-Tac-Toe View Controller

Single-click TicTacToeViewController.m. There's a lot of code to write in this controller class, so let's just replace this file with the following version:

#import "TicTacToeViewController.h"
#import "TicTacToePacket.h"

@implementation TicTacToeViewController

#pragma mark -
#pragma mark Synthesized Properties
@synthesize newGameButton;
@synthesize feedbackLabel;
@synthesize session;
@synthesize peerID;
@synthesize xPieceImage;
@synthesize oPieceImage;
#pragma mark -
#pragma mark Game-Specific Methods

- (IBAction)newGameButtonPressed {
    dieRollReceived = NO;
    dieRollAcknowledged = NO;

    newGameButton.hidden = YES;
    GKPeerPickerController *picker = [[GKPeerPickerController alloc] init];

    picker.delegate = self;

    [picker show];
}

- (IBAction)gameSpacePressed:(id)sender {
    UIButton *buttonPressed = (UIButton *)sender;
    if (state == kGameStateMyTurn &&
        [buttonPressed imageForState:UIControlStateNormal] == nil) {
        [buttonPressed setImage:(piece == kPlayerPieceO) ? oPieceImage : xPieceImage
            forState:UIControlStateNormal];
        feedbackLabel.text = NSLocalizedString(@"Opponent's Turn",
            @"Opponent's Turn");
        state = kGameStateOpponentTurn;

        TicTacToePacket *packet = [[TicTacToePacket alloc]
            initMovePacketWithSpace:buttonPressed.tag];
        [self sendPacket:packet];
        [packet release];

        [self checkForGameEnd];
    }
}

- (void)startNewGame {
    [self resetBoard];
    [self sendDieRoll];
}
- (void)resetBoard {
    for (int i = kUpperLeft; i <= kLowerRight; i++) {
        UIButton *oneButton = (UIButton *)[self.view viewWithTag:i];
        [oneButton setImage:nil forState:UIControlStateNormal];
    }

    feedbackLabel.text = @"";

    TicTacToePacket *resetPacket = [[TicTacToePacket alloc] initResetPacket];
    [self sendPacket:resetPacket];
    [resetPacket release];

    piece = kPlayerPieceUndecided;
}

- (void)resetDieState {
    dieRollReceived = NO;
    dieRollAcknowledged = NO;
    myDieRoll = kDiceNotRolled;
    opponentDieRoll = kDiceNotRolled;
}

- (void)startGame {
    if (myDieRoll == opponentDieRoll) {
        myDieRoll = kDiceNotRolled;
        opponentDieRoll = kDiceNotRolled;
        [self sendDieRoll];
        piece = kPlayerPieceUndecided;
    }
    else if (myDieRoll < opponentDieRoll) {
        state = kGameStateOpponentTurn;
        piece = kPlayerPieceX;
        feedbackLabel.text = NSLocalizedString(@"Opponent's Turn",
            @"Opponent's Turn");

    }
    else {
        state = kGameStateMyTurn;
        piece = kPlayerPieceO;
        feedbackLabel.text = NSLocalizedString(@"Your Turn", @"Your Turn");
    }
    [self resetDieState];
}

- (void)checkForGameEnd {
    NSInteger moves = 0;

    UIImage     *currentButtonImages[9];
    UIImage     *winningImage = nil;

    for (int i = kUpperLeft; i <= kLowerRight; i++) {
        UIButton *oneButton = (UIButton *)[self.view viewWithTag:i];
        if ([oneButton imageForState:UIControlStateNormal])
            moves++;
        currentButtonImages[i - kUpperLeft] = [oneButton
            imageForState:UIControlStateNormal];
}

    // Top Row
    if (currentButtonImages[0] == currentButtonImages[1] &&
        currentButtonImages[0] == currentButtonImages[2] &&
        currentButtonImages[0] != nil)
        winningImage = currentButtonImages[0];

 // Middle Row
    else if (currentButtonImages[3] == currentButtonImages[4] &&
             currentButtonImages[3] == currentButtonImages[5] &&
             currentButtonImages[3] != nil)
        winningImage = currentButtonImages[3];

    // Bottom Row
    else if (currentButtonImages[6] == currentButtonImages[7] &&
             currentButtonImages[6] == currentButtonImages[8] &&
             currentButtonImages[6] != nil)
        winningImage = currentButtonImages[6];

    // Left Column
    else if (currentButtonImages[0] == currentButtonImages[3] &&
             currentButtonImages[0] == currentButtonImages[6] &&
             currentButtonImages[0] != nil)
        winningImage = currentButtonImages[0];

    // Middle Column
    else if (currentButtonImages[1] == currentButtonImages[4] &&
             currentButtonImages[1] == currentButtonImages[7] &&
             currentButtonImages[1] != nil)
        winningImage = currentButtonImages[1];

    // Right Column
    else if (currentButtonImages[2] == currentButtonImages[5] &&
             currentButtonImages[2] == currentButtonImages[8] &&
             currentButtonImages[2] != nil)
        winningImage = currentButtonImages[2];
    // Diagonal starting top left
    else if (currentButtonImages[0] == currentButtonImages[4] &&
             currentButtonImages[0] == currentButtonImages[8] &&
             currentButtonImages[0] != nil)
        winningImage = currentButtonImages[0];

  // Diagonal starting top right
    else if (currentButtonImages[2] == currentButtonImages[4] &&
             currentButtonImages[2] == currentButtonImages[6] &&
             currentButtonImages[2] != nil)
        winningImage = currentButtonImages[2];


    if (winningImage == xPieceImage) {
        if (piece == kPlayerPieceX) {
            feedbackLabel.text = NSLocalizedString(@"You Won!", @"You Won!");
            state = kGameStateDone;
        }
        else {
            feedbackLabel.text = NSLocalizedString(@"Opponent Won!",
@"Opponent Won!");
            state = kGameStateDone;
        }
    }
    else if (winningImage == oPieceImage) {
        if (piece == kPlayerPieceO){
            feedbackLabel.text = NSLocalizedString(@"You Won!", @"You Won!");
            state = kGameStateDone;
        }
        else {
            feedbackLabel.text = NSLocalizedString(@"Opponent Won!",
                @"Opponent Won!");
            state = kGameStateDone;
        }

    }
    else {
        if (moves >= 9) {
            feedbackLabel.text = NSLocalizedString(@"Cat Wins!", @"Cat Wins!");
            state = kGameStateDone;
        }
    }

    if (state == kGameStateDone)
        [self performSelector:@selector(startNewGame) withObject:nil
            afterDelay:3.0];
}

#pragma mark -
#pragma mark Superclass Overrides
- (void)viewDidLoad {
    [super viewDidLoad];
    myDieRoll = kDiceNotRolled;
    self.oPieceImage = [UIImage imageNamed:@"O.png"];
    self.xPieceImage = [UIImage imageNamed:@"X.png"];
}

- (void)viewDidUnload {
    [super viewDidUnload];
    self.newGameButton = nil;
    self.xPieceImage = nil;
    self.oPieceImage = nil;
}

- (void)dealloc {
    [newGameButton release];
    [feedbackLabel release];
    [xPieceImage release];
    [oPieceImage release];

    session.available = NO;
    [session disconnectFromAllPeers];
    [session setDataReceiveHandler: nil withContext: nil];
    session.delegate = nil;
    [session release];
    [peerID release];
    [super dealloc];
}

#pragma mark -
#pragma mark GameKit Peer Picker Delegate Methods
- (GKSession *)peerPickerController:(GKPeerPickerController *)picker sessionForConnectionType:(GKPeerPickerConnectionType)type{
    GKSession *theSession = [[GKSession alloc]
        initWithSessionID:kTicTacToeSessionID displayName:nil
        sessionMode:GKSessionModePeer];
    return [theSession autorelease];
}

- (void)peerPickerController:(GKPeerPickerController *)picker
didConnectPeer:(NSString *)thePeerID toSession:(GKSession *)theSession {
    self.peerID = thePeerID;

    self.session = theSession;
    self.session.delegate = self;
    [self.session setDataReceiveHandler:self withContext:NULL];

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

    [self startNewGame];
}

- (void)peerPickerControllerDidCancel:(GKPeerPickerController *)picker {
    newGameButton.hidden = NO;
}

#pragma mark -
#pragma mark GameKit Session Delegate Methods
- (void)session:(GKSession *)theSession didFailWithError:(NSError *)error {
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:
        NSLocalizedString(@"Error Connecting!", @"Error Connecting!")
        message:NSLocalizedString(@"Unable to establish the connection.",
            @"Unable to establish the connection.")
        delegate:self
        cancelButtonTitle:NSLocalizedString(@"Bummer", @"Bummer")
        otherButtonTitles:nil];
    [alert show];
    [alert release];
    theSession.available = NO;
    [theSession disconnectFromAllPeers];
    theSession.delegate = nil;
    [theSession setDataReceiveHandler:nil withContext:nil];
    self.session = nil;
}

- (void)session:(GKSession *)theSession peer:(NSString *)peerID
didChangeState:(GKPeerConnectionState)inState {
    if (inState == GKPeerStateDisconnected) {
        state = kGameStateInterrupted;
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:
            NSLocalizedString(@"Peer Disconnected!", @"Peer Disconnected!")
            message:NSLocalizedString(@"Your opponent has disconnected, or 
Implementing the Tic-Tac-Toe View Controller
the connection has been lost",
                @"Your opponent has disconnected, or the connection has been lost")
            delegate:self
            cancelButtonTitle:NSLocalizedString(@"Bummer", @"Bummer")
            otherButtonTitles:nil];
        [alert show];
        [alert release];
        theSession.available = NO;
        [theSession disconnectFromAllPeers];
        theSession.delegate = nil;
        [theSession setDataReceiveHandler:nil withContext:nil];
        self.session = nil;
    }
}

#pragma mark -
#pragma mark GameKit Send & Receive Methods
- (void)sendDieRoll {
    state = kGameStateRollingDice;
    TicTacToePacket *rollPacket;
    if (myDieRoll == kDiceNotRolled) {
        rollPacket = [[TicTacToePacket alloc] initDieRollPacket];
        myDieRoll = rollPacket.dieRoll;
    }
    else
        rollPacket = [[TicTacToePacket alloc] initDieRollPacketWithRoll:myDieRoll];
    [self sendPacket:rollPacket];
    [rollPacket release];

}

- (void) receiveData:(NSData *)data fromPeer:(NSString *)peer
inSession:(GKSession *)theSession context:(void *)context
{
    NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc]
        initForReadingWithData:data];
    TicTacToePacket *packet = [unarchiver decodeObjectForKey:kTicTacToeArchiveKey];

    switch (packet.type) {
        case kPacketTypeDieRoll:
            opponentDieRoll = packet.dieRoll;
            TicTacToePacket *ack = [[TicTacToePacket alloc]
                initAckPacketWithDieRoll:opponentDieRoll];
            [self sendPacket:ack];
            [ack release];
            dieRollReceived = YES;
            break;
        case kPacketTypeAck:
            if (packet.dieRoll != myDieRoll) {
                NSLog(@"Ack packet doesn't match opponentDieRoll (mine: %d, 
Implementing the Tic-Tac-Toe View Controller
send: %d", packet.dieRoll, myDieRoll); } dieRollAcknowledged = YES; break; case kPacketTypeMove:{ UIButton *theButton = (UIButton *)[self.view viewWithTag:packet.space]; [theButton setImage:(piece == kPlayerPieceO) ? xPieceImage : oPieceImage
forState:UIControlStateNormal];
            state = kGameStateMyTurn;
            feedbackLabel.text = NSLocalizedString(@"Your Turn", @"Your Turn");
            [self checkForGameEnd];
        }
            break;
        case kPacketTypeReset:
            if (state == kGameStateDone)
                [self resetDieState];
            break;
        default:
            break;
    }

    if (dieRollReceived == YES && dieRollAcknowledged == YES)
        [self startGame];

    [unarchiver release];
}

- (void) sendPacket:(TicTacToePacket *)packet {
    NSMutableData *data = [[NSMutableData alloc] init];
    NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc]
        initForWritingWithMutableData:data];
    [archiver encodeObject:packet forKey:kTicTacToeArchiveKey];
    [archiver finishEncoding];

    NSError *error = nil;

    if (![session sendDataToAllPeers:data withDataMode:GKSendDataReliable
        error:&error]) {
        // You will do real error handling
        NSLog(@"Error sending data: %@", [error localizedDescription]);
    }
    [archiver release];
    [data release];
}

#pragma mark -
#pragma mark Alert View Delegate Methods
- (void)alertView:(UIAlertView *)alertView willDismissWithButtonIndex:(NSInteger)buttonIndex {
    [self resetBoard];
    newGameButton.hidden = NO;
}

@end

Whoa. Deep breath now. That was a lot of code, huh? Let's break it down.

The first method we wrote is the action method that is called when the New Game button is pressed. When that happens, we set dieRollReceived and dieRollAcknowledged to NO, because we know neither of these things has happened yet for the new game.

- (IBAction)newGameButtonPressed {
    dieRollReceived = NO;
dieRollAcknowledged = NO;

Next, we hide the button, because we don't want our player to request a new game while we're looking for peers or playing the game. Then we create an instance of GKPeerPickerController, set self as the delegate, and show the peer picker controller.

newGameButton.hidden = YES;
    GKPeerPickerController *picker = [[GKPeerPickerController alloc] init];

    picker.delegate = self;

    [picker show];
}

That's all we need to do to kick off the process of letting the user select another device to play against. The peer picker will handle everything, and then call delegate methods when we need to take some action.

We also wrote an action method that is called when the user taps one of the nine game spaces. The first thing we do is cast sender to a UIButton. We know sender will always be an instance of UIButton, and doing this will prevent us from needing to cast sender every time we use it.

- (IBAction)gameSpacePressed:(id)sender {
    UIButton *buttonPressed = (UIButton *)sender;

Next, we check the game state. We don't want to let the user select a space if it's not that player's turn. We also check to make sure that the button pressed has no image already assigned. If it has an image assigned to it, then there's already either an X or an O in the space this button represents, and the user is not allowed to select it.

if (state == kGameStateMyTurn &&
        [buttonPressed imageForState:UIControlStateNormal] == nil) {

If the space has no image assigned and it is our turn, we set the image to whichever image is appropriate for our player, based on whether we went first or second. The piece variable will get set later when we compare die rolls. We also set the feedback label to inform the users that it's no longer their turn, and change the state to reflect that as well.

[buttonPressed setImage:(piece == kPlayerPieceO) ? oPieceImage : xPieceImage
            forState:UIControlStateNormal];
        feedbackLabel.text = NSLocalizedString(@"Opponent's Turn",
            @"Opponent's Turn");
        state = kGameStateOpponentTurn;

We must inform the other device that we've made our move, so we create an instance of TicTacToePacket, passing the tag value from the button that was pressed to identify which space our player selected. We use a method called sendPacket:, which we'll look at in a moment, to send the instance of TicTacToePacket to the other node, and then we release packet:

TicTacToePacket *packet = [[TicTacToePacket alloc]
            initMovePacketWithSpace:buttonPressed.tag];
        [self sendPacket:packet];
        [packet release];

Finally, we check to see if the game is over. The method checkForGameEnd determines if either player won or if there are no spaces on the board, which would mean it's a tie.

[self checkForGameEnd];
    }
}

The method startNewGame is very simple. It just calls a method to reset the board, and then calls another method to roll the die and send the result to the other node. Both of these actions can happen at times other than game start. For example, we reset the board if the connection is lost, and we send the die roll if both nodes roll the same number.

- (void)startNewGame {
    [self resetBoard];
    [self sendDieRoll];
}

Resetting the board involves removing the images from all of the buttons that represent spaces on the game board. Rather than declare nine outlets—one to point at each button—we just loop through the nine tag values and retrieve the buttons from our content view using viewWithTag:.

- (void)resetBoard {
    for (int i = kUpperLeft; i <= kLowerRight; i++) {
        UIButton *oneButton = (UIButton *)[self.view viewWithTag:i];
        [oneButton setImage:nil forState:UIControlStateNormal];
    }

We also blank out the feedback label.

feedbackLabel.text = @"";

And we send a packet to the other node telling it that we're resetting. This is done just to make sure that if we follow up with another die roll, the other machine knows not to overwrite it. The fact that network communication happens asynchronously means we can't rely on things always happening in a specific order, as we can with a program running on only one device. It's possible that we'll send the die roll before the other device has finished determining who won. By sending a reset packet, we tell the other node that there may be another die roll coming for a new game, so make sure it's in the right state to accept that new roll. If we didn't do something like this, it might store our die roll, and then overwrite the rolled value when it resets its own board, which would cause a hang because the other device would then be waiting for a die roll that would never arrive.

TicTacToePacket *resetPacket = [[TicTacToePacket alloc] initResetPacket];
    [self sendPacket:resetPacket];
    [resetPacket release];

We also need to reset the player's game piece. Because the game is over, we don't know if the player will be X or O for the next game.

piece = kPlayerPieceUndecided;
}

Resetting the die state is nothing more than setting dieRollReceived and dieRollAcknowledged to NO, and setting both our die roll and the opponent's die roll to kDiceNotRolled:

- (void)resetDieState {
    dieRollReceived = NO;
    dieRollAcknowledged = NO;
    myDieRoll = kDiceNotRolled;
    opponentDieRoll = kDiceNotRolled;
}

The next method is called once we have received our opponent's die roll and have also gotten an acknowledgment that it has received ours. First, we make sure that we don't have a tie. If we do have a tie, we kick off the die-rolling process again.

- (void)startGame {
    if (myDieRoll == opponentDieRoll) {
        myDieRoll = kDiceNotRolled;
        opponentDieRoll = kDiceNotRolled;
        [self sendDieRoll];
        piece = kPlayerPieceUndecided;
    }

Otherwise, we set state, piece, and the feedbackLabel's text based on whether it's our turn or the opponent's turn to go first.

else if (myDieRoll < opponentDieRoll) {
        state = kGameStateOpponentTurn;
        piece = kPlayerPieceX;
        feedbackLabel.text = NSLocalizedString(@"Opponent's Turn",
            @"Opponent's Turn");
    }
    else {
        state = kGameStateMyTurn;
        piece = kPlayerPieceO;
        feedbackLabel.text = NSLocalizedString(@"Your Turn", @"Your Turn");
    }

Then we reset the die state. It may seem odd to do it here, but at this point, we're finished with the die rolling for this game, and because we may receive our opponent's die roll before our code has realized the game is over, we reset now to ensure that the die rolls are not accidentally reused in the next game.

[self resetDieState];
}

The checkForGameEnd method just checks all nine spaces to see whether they have X or O in them, and then looks for three in a row. It does this by first declaring a variable called moves to keep track of how many moves have happened. This is how it will tell if there's a tie. If there have been nine moves, and no one has won, then there are no available spaces left on the board, so it's a tie.

- (void)checkForGameEnd {
    NSInteger moves = 0;

Next, we declare an array of nine UIImage pointers. We're going to pull the images out of the nine buttons representing spaces on the board and put them in this array to make it easier to check if a player won.

UIImage     *currentButtonImages[9];

If we find three in a row, we'll store one of the three images in this variable so we know which player won the game.

UIImage     *winningImage = nil;

Next, we loop through the buttons by tag, as we did in the resetBoard method earlier, storing the images from the buttons in the array we declared earlier.

for (int i = kUpperLeft; i <= kLowerRight; i++) {
        UIButton *oneButton = (UIButton *)[self.view viewWithTag:i];
        if ([oneButton imageForState:UIControlStateNormal])
            moves++;
        currentButtonImages[i - kUpperLeft] = [oneButton
            imageForState:UIControlStateNormal];
    }

The next big chunk of code just checks to see if there are three of the same images in a row anywhere. If it finds three in a row, it stores one of the three images in winningImage. When it completes the check, it will know which player, if any, has won.

// Top Row
    if (currentButtonImages[0] == currentButtonImages[1] &&
        currentButtonImages[0] == currentButtonImages[2] &&
        currentButtonImages[0] != nil)
        winningImage = currentButtonImages[0];

   // Middle Row
    else if (currentButtonImages[3] == currentButtonImages[4] &&
             currentButtonImages[3] == currentButtonImages[5] &&
             currentButtonImages[3] != nil)
        winningImage = currentButtonImages[3];

    // Bottom Row
    else if (currentButtonImages[6] == currentButtonImages[7] &&
             currentButtonImages[6] == currentButtonImages[8] &&
             currentButtonImages[6] != nil)
        winningImage = currentButtonImages[6];

   // Left Column
    else if (currentButtonImages[0] == currentButtonImages[3] &&
             currentButtonImages[0] == currentButtonImages[6] &&
             currentButtonImages[0] != nil)
        winningImage = currentButtonImages[0];

    // Middle Column
    else if (currentButtonImages[1] == currentButtonImages[4] &&
             currentButtonImages[1] == currentButtonImages[7] &&
             currentButtonImages[1] != nil)
        winningImage = currentButtonImages[1];

    // Right Column
    else if (currentButtonImages[2] == currentButtonImages[5] &&
currentButtonImages[2] == currentButtonImages[8] &&
             currentButtonImages[2] != nil)
        winningImage = currentButtonImages[2];
    // Diagonal starting top left
    else if (currentButtonImages[0] == currentButtonImages[4] &&
             currentButtonImages[0] == currentButtonImages[8] &&
             currentButtonImages[0] != nil)
        winningImage = currentButtonImages[0];

   // Diagonal starting top right
    else if (currentButtonImages[2] == currentButtonImages[4] &&
             currentButtonImages[2] == currentButtonImages[6] &&
             currentButtonImages[2] != nil)
        winningImage = currentButtonImages[2];

Finally, we check to see if there was a winner, and whether it was our opponent or our player. If there is a winner, we set the feedback label and state as appropriate.

if (winningImage == xPieceImage) {
        if (piece == kPlayerPieceX) {
            feedbackLabel.text = NSLocalizedString(@"You Won!", @"You Won!");
            state = kGameStateDone;
        }
        else {
            feedbackLabel.text = NSLocalizedString(@"Opponent Won!",
                @"Opponent Won!");
            state = kGameStateDone;
        }
    }
    else if (winningImage == oPieceImage) {
        if (piece == kPlayerPieceO){
            feedbackLabel.text = NSLocalizedString(@"You Won!", @"You Won!");
            state = kGameStateDone;
        }
        else {
            feedbackLabel.text = NSLocalizedString(@"Opponent Won!",
                @"Opponent Won!");
            state = kGameStateDone;
        }
    }

If there wasn't a winner, then we check to see if any spaces are left on the board by looking at moves. If no spaces remain, then we know the game is over, and the cat won.

else {
        if (moves >= 9) {
            feedbackLabel.text = NSLocalizedString(@"Cat Wins!", @"Cat Wins!");
            state = kGameStateDone;
        }
    }

Note

In tic-tac-toe, a tie is also called a "cat's game." The expression "the cat won" refers to a tie.

If any of the preceding code set the state to kGameStateDone, then we use performSelector:withObject:afterDelay: to start a new game after the user has had time to read who won.

if (state == kGameStateDone)
        [self performSelector:@selector(startNewGame) withObject:nil
            afterDelay:3.0];
}

In viewDidLoad, we first set myDieRoll to show that we have not yet rolled the die to choose who goes first. Then we load the two images used for the playing pieces and store them in the two properties designed to hold them.

- (void)viewDidLoad {
    [super viewDidLoad];
    myDieRoll = kDiceNotRolled;
    self.oPieceImage = [UIImage imageNamed:@"O.png"];
    self.xPieceImage = [UIImage imageNamed:@"X.png"];
}

The viewDidUnload method is pretty typical, so it doesn't warrant any discussion.

- (void)viewDidUnload {
    [super viewDidUnload];
    self.newGameButton = nil;
    self.xPieceImage = nil;
    self.oPieceImage = nil;
}

Most of the dealloc method is pretty standard, too. Just notice that before we release session, we take care of disconnecting from our peers and setting both the delegate and data receive handler to nil, as we discussed earlier in the chapter.

- (void)dealloc {
    [newGameButton release];

    [xPieceImage release];
    [oPieceImage release];

    session.available = NO;
    [session disconnectFromAllPeers];
    [session setDataReceiveHandler: nil withContext: nil];
    session.delegate = nil;
    [session release];

    [super dealloc];
}

Now, we get into the peer picker delegate methods. This first one is where the picker asks us to provide a session. Because we want all devices to both advertise and look for other devices on the network, we specify GKSessionModePeer for the session mode. Notice that we also use our constant kTicTacToeSessionID, which we defined in the header file to make sure that we connect only to other instances of TicTacToe.

- (GKSession *)peerPickerController:(GKPeerPickerController *)picker sessionForConnectionType:(GKPeerPickerConnectionType)type{
    GKSession *theSession = [[GKSession alloc]
initWithSessionID:kTicTacToeSessionID displayName:nil
        sessionMode:GKSessionModePeer];
    return [theSession autorelease];
}

Because the peer picker is only for simple peer-to-peer games, once we're notified of a connection, we store the session and the peer identifier, and then dismiss the picker. After we've dismissed it, we call startNewGame to get things going.

- (void)peerPickerController:(GKPeerPickerController *)picker
didConnectPeer:(NSString *)thePeerID toSession:(GKSession *)theSession {
    self.peerID = thePeerID;

    self.session = theSession;
    self.session.delegate = self;
    [self.session setDataReceiveHandler:self withContext:NULL];

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

    [self startNewGame];
}

This method is called if the users select Cancel from either the Bluetooth enable or peer picker dialog. It simply makes sure that our New Game button is visible if they cancel, so they can still start a new game.

- (void)peerPickerControllerDidCancel:(GKPeerPickerController *)picker {
    newGameButton.hidden = NO;
}

The next few methods are the session delegate methods. The first one we implement is called if a connection attempt fails. All we do is put up an alert view informing the user that it failed, and then clean up the session. In the alert view delegate method, we reset the board so the users can try again or select a new opponent if they want.

- (void)session:(GKSession *)theSession didFailWithError:(NSError *)error {
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:
        NSLocalizedString(@"Error Connecting!", @"Error Connecting!")
        message:NSLocalizedString(@"Unable to establish the connection.",
            @"Unable to establish the connection.")
        delegate:self
        cancelButtonTitle:NSLocalizedString(@"Bummer", @"Bummer")
        otherButtonTitles:nil];
    [alert show];
    [alert release];
    theSession.available = NO;
    [theSession disconnectFromAllPeers];
    theSession.delegate = nil;
    [theSession setDataReceiveHandler:nil withContext:nil];
    self.session = nil;
}

Because we're using the peer picker, we don't need to handle choosing another node or connecting to it. But we must make sure that if the opponent disconnects, we don't keep trying to play that game. The following method is called any time a peer's state changes. If we're notified that another node has disconnected, we again inform the users through an alert view, and when they dismiss it, our alert view delegate method will reset the board.

- (void)session:(GKSession *)theSession peer:(NSString *)peerID
        didChangeState:(GKPeerConnectionState)inState {
    if (inState == GKPeerStateDisconnected) {
        state = kGameStateInterrupted;
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:
            NSLocalizedString(@"Peer Disconnected!", @"Peer Disconnected!")
            message:NSLocalizedString(@"Your opponent has disconnected, or 
Implementing the Tic-Tac-Toe View Controller
the connection has been lost", @"Your opponent has disconnected, or the connection has been lost") delegate:self cancelButtonTitle:NSLocalizedString(@"Bummer", @"Bummer") otherButtonTitles:nil]; [alert show]; [alert release]; theSession.available = NO; [theSession disconnectFromAllPeers]; theSession.delegate = nil; [theSession setDataReceiveHandler:nil withContext:nil]; self.session = nil; } }

This method sends a die roll packet to the other node. The initDieRollPacket method automatically generates a packet with a random number.

- (void)sendDieRoll {
    state = kGameStateRollingDice;
    TicTacToePacket *rollPacket;
    if (myDieRoll == kDiceNotRolled) {
        rollPacket = [[TicTacToePacket alloc] initDieRollPacket];
        myDieRoll = rollPacket.dieRoll;
    }
    else
        rollPacket = [[TicTacToePacket alloc] initDieRollPacketWithRoll:myDieRoll];
    [self sendPacket:rollPacket];
    [rollPacket release];
}

The following is our data receive handler. This method is called whenever we receive a packet from the other node. The first thing we do is unarchive the data into a copy of the original TicTacToePacket instance that was sent.

- (void) receiveData:(NSData *)data fromPeer:(NSString *)peer
        inSession:(GKSession *)theSession context:(void *)context {
    NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc]
        initForReadingWithData:data];
    TicTacToePacket *packet = [unarchiver decodeObjectForKey:kTicTacToeArchiveKey];

Then we use a switch statement to take different actions based on the type of packet we received.

switch (packet.type) {

If it's a die roll, we store our opponent's value, send back an acknowledgment of the value, and set dieRollReceived to YES.

case kPacketTypeDieRoll:
            opponentDieRoll = packet.dieRoll;
            TicTacToePacket *ack = [[TicTacToePacket alloc]
                initAckPacketWithDieRoll:opponentDieRoll];
            [self sendPacket:ack];
            [ack release];
            dieRollReceived = YES;
            break;

If we've received an acknowledgment, we make sure the number returned is the same as the one we sent. This is just a consistency check. It shouldn't ever happen that the number is not the same. If it did, it might be an indication of a problem with our code, or it could mean that someone is cheating. Although we doubt that anyone would bother cheating at tic-tac-toe, people have been know to cheat in some networked games, so you might want to consider validating any information exchanged with peers. Here, we're just logging the inconsistency and moving on. In your real-world applications, you might want to take more serious action if you detect a data inconsistency of this nature.

case kPacketTypeAck:
            if (packet.dieRoll != myDieRoll) {
                NSLog(@"Ack packet doesn't match opponentDieRoll (mine: %d, 
Implementing the Tic-Tac-Toe View Controller
send: %d", packet.dieRoll, myDieRoll); } dieRollAcknowledged = YES; break;

If the packet is a move packet, which denotes that the other player chose a space, we update the appropriate space with an X or O image, and change the state and label to reflect the fact that it's now our player's turn. We also check to see if the other player's move resulted in the game being over.

case kPacketTypeMove:{
            UIButton *theButton = (UIButton *)[self.view viewWithTag:packet.space];
            [theButton setImage:(piece == kPlayerPieceO) ? xPieceImage : oPieceImage
                forState:UIControlStateNormal];
            state = kGameStateMyTurn;
            feedbackLabel.text = NSLocalizedString(@"Your Turn", @"Your Turn");
            [self checkForGameEnd];
        }
            break;

When we receive a reset packet, all we do is change the game state to kGameStateDone, so that if a die roll comes in before we've realized the game is over, we don't discard it.

case kPacketTypeReset:
            if (state == kGameStateDone)
                [self resetDieState];
        default:
            break;
    }

If we received a packet, and both dieRollReceived and dieRollAcknowledged are now YES, we know it's time to start the game.

if (dieRollReceived == YES && dieRollAcknowledged == YES)
        [self startGame];

Of course, before our method is complete, we need to release the unarchiver, since we used alloc to create it.

[unarchiver release];
}

The next method sends a packet to the other device. It takes an instance of TicTacToePacket and archives it into an instance of NSData. It then uses the session's sendDataToAllPeers:withDataMode:error: method to send it across the wire—well, across the wireless, in this case.

- (void) sendPacket:(TicTacToePacket *)packet {
    NSMutableData *data = [[NSMutableData alloc] init];
    NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc]
        initForWritingWithMutableData:data];
    [archiver encodeObject:packet forKey:kTicTacToeArchiveKey];
    [archiver finishEncoding];

    NSError *error = nil;

    if (![session sendDataToAllPeers:data withDataMode:GKSendDataReliable
        error:&error]) {
        // You will do real error handling
        NSLog(@"Error sending data: %@", [error localizedDescription]);
    }
    [archiver release];
    [data release];
}

The last method in our controller class is the alert view delegate. This is called any time we show an alert view. The only reason we ever show an alert view in this application is to inform the user that something bad happened. Therefore, if we get here, we know we must reset the board and show the New Game button.

- (void)alertView:(UIAlertView *)alertView willDismissWithButtonIndex:(NSInteger)buttonIndex {
    [self resetBoard];
    newGameButton.hidden = NO;
}
@end

Trying It Out

Unlike most of the applications we've written together, our tic-tac-toe game can't be used in the simulator. It will run there, but the simulator does not support Bluetooth connections. Our app currently relies on Bluetooth connections to work, since we're using GameKit and the peer picker. As a result, you'll need to have two physical devices, and neither of them can be a first-generation device, because the original iPhone and the first-generation iPod touch do not work with GameKit's peer picker.

It also means that you need to have two devices provisioned for development, but note that you do not want to connect both devices to your computer at the same time. This can cause some problems, since there's no way to specify which one to use for debugging. Therefore, you need to build and run on one device, quit, unplug that device, and then plug in the other device and do the same thing. Once you've done that, you will have the application on both devices. You can run it on both devices, or you can launch it from Xcode on one device, so you can debug and read the console feedback.

Note

Detailed instructions for installing applications on a device are available at http://developer.apple.com/iphone in the developer portal, which is available only to paid iPhone SDK members.

You should be aware that debugging—or even running from Xcode without debugging—will slow down the program running on the connected iPhone, and this can have an affect on network communications. Underneath the hood, all of the data transmissions back and forth between the two devices check for acknowledgments and have a timeout period. If they don't receive a response in a certain amount of time, they will disconnect. So, if you set a breakpoint, chances are that you will break the connection between the two devices when it reaches the breakpoint. This can make figuring out problems in your GameKit application tedious. You often will need to use alternatives to breakpoints, like NSLog() or breakpoint actions, so you don't break the network connection between the devices. We'll talk more about debugging in Chapter 15.

Game On!

Another long chapter under your belt, and you should now have a pretty firm understanding of GameKit networking. You've seen how to use the peer picker to let your user select another iPhone or iPod touch to which to connect. You've seen how to send data by archiving objects, and you've gotten a little taste of the complexity that is introduced to your application when you start adding in network multiuser functionality.

In the next chapter, we're going to expand the TicTacToe application to support online play over Wi-Fi using Bonjour. So when you've recovered, skip on over to the next page, and we'll get started.

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

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