Game Center Tutorial for iOS: How To Make A Simple Multiplayer Game: Part 1/2

The first part of a Game Center tutorial series that shows you how to create a simple multiplayer iPhone game. By Ray Wenderlich.

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

Authenticate the Local User: Implementation

In the Cat Race Xcode project, go to File\New\New File, choose iOS\Cocoa Touch\Objective-C class, and click Next. Enter NSObject for Subclass of, click Next, name the new class GCHelper.m, and click Finish.

Replace GCHelper.h with the following:

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

@interface GCHelper : NSObject {
    BOOL gameCenterAvailable;
    BOOL userAuthenticated;
}

@property (assign, readonly) BOOL gameCenterAvailable;

+ (GCHelper *)sharedInstance;
- (void)authenticateLocalUser;

@end

This imports the GameKit header file, and then creates an object with two booleans – one to keep track of if game center is available on this device, and one to keep track of whether the user is currently authenticated.

It also creates a property so the game can tell if game center is available, a static method to retrieve the singleton instance of this class, and another method to authenticate the local user (which will be called when the app starts up).

Next switch to GCHelper.m and add the following right inside the @implementation:

@synthesize gameCenterAvailable;

#pragma mark Initialization

static GCHelper *sharedHelper = nil;
+ (GCHelper *) sharedInstance {
    if (!sharedHelper) {
        sharedHelper = [[GCHelper alloc] init];
    }
    return sharedHelper;
}

This synthesizes the gameCenterAvailable property, then defines the method to create the singleton instance of this class.

Note there are many ways of writing singleton methods, but this is the simplest way when you don’t have to worry about multiple threads trying to initialize the singleton at the same time.

Next add the following method right after the sharedInstance method:

- (BOOL)isGameCenterAvailable {
    // check for presence of GKLocalPlayer API
    Class gcClass = (NSClassFromString(@"GKLocalPlayer"));
	
    // check if the device is running iOS 4.1 or later
    NSString *reqSysVer = @"4.1";
    NSString *currSysVer = [[UIDevice currentDevice] systemVersion];
    BOOL osVersionSupported = ([currSysVer compare:reqSysVer 
        options:NSNumericSearch] != NSOrderedAscending);
	
    return (gcClass && osVersionSupported);
}

This method is straight from Apple’s Game Kit Programming Guide. It’s the way to check if Game Kit is available on the current device.

By making sure Game Kit is available before using it, this app can still run on iOS 4.0 or earlier (just without network capabilities).

Next add the following right after the isGameCenterAvailable method:

- (id)init {
    if ((self = [super init])) {
        gameCenterAvailable = [self isGameCenterAvailable];
        if (gameCenterAvailable) {
            NSNotificationCenter *nc = 
            [NSNotificationCenter defaultCenter];
            [nc addObserver:self 
                   selector:@selector(authenticationChanged) 
                       name:GKPlayerAuthenticationDidChangeNotificationName 
                     object:nil];
        }
    }
    return self;
}

- (void)authenticationChanged {    
    
    if ([GKLocalPlayer localPlayer].isAuthenticated && !userAuthenticated) {
       NSLog(@"Authentication changed: player authenticated.");
       userAuthenticated = TRUE;           
    } else if (![GKLocalPlayer localPlayer].isAuthenticated && userAuthenticated) {
       NSLog(@"Authentication changed: player not authenticated");
       userAuthenticated = FALSE;
    }
                   
}

The init method checks to see if Game Center is available, and if so registers for the “authentication changed” notification. It’s important that the app registers for this notification before attempting to authenticate the user, so that it’s called when the authentication completes.

The authenticationChanged callback is very simple at this point – it checks to see whether the change was due to the user being authenticate or un-authenticated, and updates a status flag accordingly.

Note that in practice this might be called several times in a row for authentication or un-authentication, so by making sure the userAuthenticated flag is different than the current status, it only logs if there’s a change since last time.

Finally, add the method to authenticate the local user right after the authenticationChanged method:

#pragma mark User functions

- (void)authenticateLocalUser { 
    
    if (!gameCenterAvailable) return;
    
    NSLog(@"Authenticating local user...");
    if ([GKLocalPlayer localPlayer].authenticated == NO) {     
        [[GKLocalPlayer localPlayer] authenticateWithCompletionHandler:nil];        
    } else {
        NSLog(@"Already authenticated!");
    }
}

This calls the authenticateWithCompletionHandler method mentioned earlier to tell Game Kit to authenticate the user. Note it doesn’t pass in a completion handler. Since you’ve already registered for the “authentication changed” notification it’s not necessary.

OK – GCHelper now contains all of the code necessary to authenticate the user, so you just have to use it! Switch to AppDelegate.m and make the following changes:

// At the top of the file
#import "GCHelper.h"

// At the end of applicationDidFinishLaunching, right before 
// the last line that calls runWithScene:
[[GCHelper sharedInstance] authenticateLocalUser];

This creates the Singleton instance (which registers for the “authentication changed” callback as part of initialization), then calls the authenticateLocalUser method.

Almost done! The last step is to add the Game Kit framework into your project. To do this, select the CatRace project in the upper left of Groups & Files, select the Build Phases tab, expand the “Link Binary with Libraries” section, and click the “+” button.

Select GameKit.framework, and click Add. Change the type from Required to Optional, and your screen should look like the following:

Adding Game Kit Framework to Xcode 4 Project

That’s it! Compile and run your project, and if you’re logged into Game Center you should see something like the following:

Authenticating Local User with Game Center

Now that you’ve authenticated the user, you can start moving onto the fun stuff – such as finding someone to play with!

Matchmaker, Matchmaker, Make Me A Match

There are two ways to find someone to play with via Game Center: search for match programatically, or use the built-in matchmaking user interface.

In this tutorial, we’re going to use the built-in matchmaking user interface. The idea is when you want to find a match, you set up some parameters in a GKMatchRequest object, then create and display an instance of a GKMatchmakerViewController.

Let’s see how this works. First make a few changes to GCHelper.h:

// Add to top of file
@protocol GCHelperDelegate 
- (void)matchStarted;
- (void)matchEnded;
- (void)match:(GKMatch *)match didReceiveData:(NSData *)data 
    fromPlayer:(NSString *)playerID;
@end

// Modify @interface line to support protocols as follows
@interface GCHelper : NSObject <GKMatchmakerViewControllerDelegate, GKMatchDelegate> {

// Add inside @interface
UIViewController *presentingViewController;
GKMatch *match;
BOOL matchStarted;
id <GCHelperDelegate> delegate;

// Add after @interface
@property (retain) UIViewController *presentingViewController;
@property (retain) GKMatch *match;
@property (assign) id <GCHelperDelegate> delegate;

- (void)findMatchWithMinPlayers:(int)minPlayers maxPlayers:(int)maxPlayers 
    viewController:(UIViewController *)viewController 
    delegate:(id<GCHelperDelegate>)theDelegate;

There’s a bunch of new stuff here, so let’s go over it bit by bit.

  • You define a protocol called GCHelperDelegate that you’ll use to notify another object of when important events happen, such as the match starting, ending, or receiving data from the other party. For this game, your Cocos2D layer will be implementing this protocol.
  • The GCHelper object is marked as implementing two protocols. The first is so that the matchmaker user interface can notify this object when a match is found or not. The second is so that Game Center can notify this object when data is received or the connection status changes.
  • Creates some new instance variables and properties to keep track of a view controller that will be used to present the matchmaker user interface, a reference to the match, whether it’s started or not, and the delegate.
  • Creates a new method that the Cococs2D layer will call to look for someone to play with.

Next switch to GCHelper.m and make the following changes:

// At top of file
@synthesize presentingViewController;
@synthesize match;
@synthesize delegate;

// Add new method, right after authenticateLocalUser
- (void)findMatchWithMinPlayers:(int)minPlayers maxPlayers:(int)maxPlayers   
    viewController:(UIViewController *)viewController 
    delegate:(id<GCHelperDelegate>)theDelegate {
    
    if (!gameCenterAvailable) return;
    
    matchStarted = NO;
    self.match = nil;
    self.presentingViewController = viewController;
    delegate = theDelegate;               
    [presentingViewController dismissModalViewControllerAnimated:NO];

    GKMatchRequest *request = [[[GKMatchRequest alloc] init] autorelease]; 
    request.minPlayers = minPlayers;     
    request.maxPlayers = maxPlayers;
    
    GKMatchmakerViewController *mmvc = 
        [[[GKMatchmakerViewController alloc] initWithMatchRequest:request] autorelease];    
    mmvc.matchmakerDelegate = self;
    
    [presentingViewController presentModalViewController:mmvc animated:YES];
        
}

This is the method that the Cocos2D layer will call to find a match. It does nothing if Game Center is not available.

It initializes the match as not started yet, and the match object as nil. It stores away the view controller and delegate for later use, and dismisses any previously existing modal view controllers (in case a GKMatchmakerViewController is already showing).

Then it moves into the important stuff. The GKMatchRequest object allows you to configure the type of match you’re looking for, such as a minimum and maximum amount of players. This method sets it to whatever is passed in (which for this game will be min 2, max 2 players).

Next it creates a new instance of the GKMatchmakerViewController with the given request, sets its delegate to the GCHelper object, and uses the passed-in view controller to show it on the screen.

The GKMatchmakerViewController takes over from here, and allows the user to search for a random player and start a game. Once it’s done some callback methods will be called, so let’s add those next:

#pragma mark GKMatchmakerViewControllerDelegate

// The user has cancelled matchmaking
- (void)matchmakerViewControllerWasCancelled:(GKMatchmakerViewController *)viewController {
    [presentingViewController dismissModalViewControllerAnimated:YES];
}

// Matchmaking has failed with an error
- (void)matchmakerViewController:(GKMatchmakerViewController *)viewController didFailWithError:(NSError *)error {
    [presentingViewController dismissModalViewControllerAnimated:YES];
    NSLog(@"Error finding match: %@", error.localizedDescription);    
}

// A peer-to-peer match has been found, the game should start
- (void)matchmakerViewController:(GKMatchmakerViewController *)viewController didFindMatch:(GKMatch *)theMatch {
    [presentingViewController dismissModalViewControllerAnimated:YES];
    self.match = theMatch;
    match.delegate = self;
    if (!matchStarted && match.expectedPlayerCount == 0) {
        NSLog(@"Ready to start match!");
    }
}

If the user cancelled finding a match or there was an error, it just closes the matchmaker view.

However if a match was found, it squirrels away the match object and sets the delegate of the match to be the GCHelper object so it can be notified of incoming data and connection status changes.

It also runs a quick check to see if it’s time to actually start the match. The match object keeps track of how many players still need to finish connecting as the “expectedPlayerCount”.

If this is 0, everybody’s ready to go. Right now we’re just going to log that out – later on we’ll actually do something interesting here.

Next, add the implementation of the GKMatchDelegate callbacks:

#pragma mark GKMatchDelegate

// The match received data sent from the player.
- (void)match:(GKMatch *)theMatch didReceiveData:(NSData *)data fromPlayer:(NSString *)playerID {    
    if (match != theMatch) return;
    
    [delegate match:theMatch didReceiveData:data fromPlayer:playerID];
}

// The player state changed (eg. connected or disconnected)
- (void)match:(GKMatch *)theMatch player:(NSString *)playerID didChangeState:(GKPlayerConnectionState)state {   
    if (match != theMatch) return;
    
    switch (state) {
        case GKPlayerStateConnected: 
            // handle a new player connection.
            NSLog(@"Player connected!");
            
            if (!matchStarted && theMatch.expectedPlayerCount == 0) {
                NSLog(@"Ready to start match!");
            }
            
            break; 
        case GKPlayerStateDisconnected:
            // a player just disconnected. 
            NSLog(@"Player disconnected!");
            matchStarted = NO;
            [delegate matchEnded];
            break;
    }                     
}

// The match was unable to connect with the player due to an error.
- (void)match:(GKMatch *)theMatch connectionWithPlayerFailed:(NSString *)playerID withError:(NSError *)error {
    
    if (match != theMatch) return;
    
    NSLog(@"Failed to connect to player with error: %@", error.localizedDescription);
    matchStarted = NO;
    [delegate matchEnded];
}

// The match was unable to be established with any players due to an error.
- (void)match:(GKMatch *)theMatch didFailWithError:(NSError *)error {
    
    if (match != theMatch) return;
    
    NSLog(@"Match failed with error: %@", error.localizedDescription);
    matchStarted = NO;
    [delegate matchEnded];
}

match:didReceiveData:fromPlayer is called when another player sends data to you. This method simply forwards the data onto the delegate (which will be the Cocos2D layer in this game), so that it can do the game-specific stuff with it.

For match:player:didChangState, when the player connects you need to check if all the players have connected in, so you can start the match once they’re all in. Other than that, if a player disconnects it sets the match as ended and notifies the delegate.

The final two methods are called when there’s an error with the connection. In either case, it marks the match as ended and notifies the delegate.

OK, now that we have this code to establish a match, let’s use it in our HelloWorldLayer. Switch to HelloWorldLayer.h and make the following changes:

// Add to top of file
#import "GCHelper.h"

// Mark @interface as implementing GCHelperDelegate
@interface HelloWorldLayer : CCLayer <GCHelperDelegate>

Then switch to HelloWorldLayer.m and make the following changes:

// Add to top of file
#import "AppDelegate.h"
#import "RootViewController.h"

// Add to bottom of init method, right after setGameState
AppDelegate * delegate = (AppDelegate *) [UIApplication sharedApplication].delegate;                
[[GCHelper sharedInstance] findMatchWithMinPlayers:2 maxPlayers:2 viewController:delegate.viewController delegate:self];

// Add new methods to bottom of file
#pragma mark GCHelperDelegate

- (void)matchStarted {    
    CCLOG(@"Match started");        
}

- (void)matchEnded {    
    CCLOG(@"Match ended");    
}

- (void)match:(GKMatch *)match didReceiveData:(NSData *)data fromPlayer:(NSString *)playerID {
    CCLOG(@"Received data");
}

The most important part here is in the init method. It gets the RootViewController from the AppDelegate, because that is the view controller that will present the matchmaker view controller. Then it calls the new method you just wrote on GCHelper to find a match by presenting the matchmaker view controller.

The rest is just some stub functions when a match begins or ends that you’ll be implementing later.

One last thing. By default the Cocos2D template does not contain a property for the RootViewController in the App Delegate, so you have to add one. Switch to AppDelegate.h and add the following:

@property (nonatomic, retain) RootViewController *viewController;

And switch to AppDelegate.m and synthesize it:

@synthesize viewController;

That’s it! Compile and run your app, and you should see the matchmaker view controller start up:

GKMatchmakerViewController in Portrait Mode

Now run your app on a different device so you have two running at the same time (i.e. maybe your simulator and your iPhone).

Important: Make sure you are using a different Game Center account on each device, or it won’t work!

Click “Play Now” on both devices, and after a little bit of time, the matchkaker view controller should go away, and you should see something like this in your console log:

CatRace[16440:207] Authentication changed: player authenticated.
CatRace[16440:207] Player connected!
CatRace[16440:207] Ready to start match!

Congrats – you now have made a match between two devices! You’re on your way to making a networked game!