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

Ray Wenderlich
Create a multiplayer racing game with Cocos2D and Game Center!

Create a multiplayer racing game with Cocos2D and Game Center!

I’m experimenting with a new way of writing tutorials – by taking suggestions by you guys!

In the sidebar to the right (scroll down a bit), you’ll find a new section where you can vote for which tutorial comes next.

In the first vote, a bunch of you guys said you wanted a tutorial about making a simple multiplayer game (88 of you to be exact!) – so here you go! :]

In this 2-part Game Center tutorial series, you’ll make a simple 2-player networked game with Cocos2D and Game Center matchmaking.

The game you’ll be working with is very simple. It’s a racing game with a dog vs. a kid – tap as fast as you can to win!

This Game Center tutorial assumes you are already familiar with the basics of using Cocos2D. If you are new to Cocos2D, you might want to check out some of the other Cocos2D tutorials on this site first.

Note: To fully get through this tutorial series, you must be a registered member of the iOS developer program so you can enable your app to use Game Center. Also, you will need at least one physical device (so that you can run one copy on the simulator and one on your device). Finally, you will need at least two different Game Center accounts for testing (don’t worry, you can create more than one for free, you just need another email address).

Ready? On your mark, get set, GO!

Getting Started

This Game Center tutorial show you how to add matchmaking and multiplayer capabilities into a simple game.

Since making the game logic itself isn’t the point of this tutorial, I’ve prepared some starter code for you that has the game without any network code.

Download the code and run the project, and you should see a screen like this:

Simple Cocos2D Racing Game Starter Code

The game is very simple and well commented – go ahead and browse through the code and make sure you understand everything.

If you guys are interested, I could do a separate tutorial sometime on how to make this game from scratch. If you would like this, please suggest this tutorial in the forums!

Enabling Game Center: Overview

At this point, you have a simple playable game, except it’s pretty boring since you’re playing all by yourself!

It would be a lot more fun to use Game Center, so you can invite friends to play with you, or use matchmaking to play with random people online.

But before you can start writing any Game Center code, you need to do two things:

  1. Create and set an App ID
  2. Register your app in iTunes Connect

Let’s go through each of these in turn.

Create and Set an App ID

The first step is to create and set an App ID. To do this, log onto the iOS Dev Center, and from there log onto the iOS Provisioning Portal.

From there, select the App IDs tab, and create a new App ID for your app, similar to the following (except you’ll be choosing different values):

Creating an App ID in the iOS Provisioning Portal

The most important part is the Bundle Identifier – you need to set this to a unique string (so it can’t be the same as the one I used!) It’s usually good practice to use a domain name you control followed by a unique string to avoid name collisions.

Once you’re done, click Submit. Then open the Cat Race Xcode project, select Resources\Info.plist, and set your Bundle identifier to whatever you entered in the iOS Provisioning portal, as shown below (except you’ll be entering a different value):

Setting the Bundle Identifier in your info.plist

One last thing. Xcode sometimes gets confused when you change your bundle identifier mid-project, so to make sure everything’s dandy take the following steps:

  • Delete any copies of Cat Race currently on your simulator or device
  • Quit your simulator if it’s running
  • Do a clean build with Project\Clean

Congrats – now you have an App ID for your app, and your app is set up to use it! Next you can register your app with iTunes Connect and enable Game Center.

Register your app in iTunes Connect

The next step is to log on to iTunes Connect and create a new entry for your app.

Once you’re logged onto iTunes Connect, select Manage Your Applications, and then click the blue “Add New App” button in the upper left.

On the first screen, enter Cat Race for the App Name, CR1 for SKU Number, and select the bundle ID you created earlier, similar to the screenshot below:

Creating a new App in iTunes Connect

Click continue, and follow the prompts to set up some basic information about your app.

Don’t worry about the exact values to put in, since it doesn’t really matter and you can change any of this later – you just need to put something (including a dummy icon and screenshot) in to make iTunes Connect happy.

When you’re done, click Save, and if all works well you should be in the “Prepare for Upload” stage and will see a screen like this:

The Manage Game Center Button in iTunes Connect

Click the blue “Manage Game Center” button to the upper right, click the big blue “Enable” button, and click “Done”. That’s it – Game Center is enabled for your app, and you’re ready to write some code!

By the way – inside the “Manage Game Center” section, you might have noticed some options to set up Leaderboards or Achievments.

We won’t be covering Leaderboards or Achievements in this tutorial, but if you are interested Rod and I cover this in our upcoming Cocos2D book.

Authenticate the Local Player: Strategy

When your game starts up, the first thing you need to do is authenticate the local player.

You can think of this as “logging the player into Game Center.” If he’s already logged in, it will say “Welcome back!” Otherwise, it will ask for the player’s username and password.

Authenticating the local user is easy – you just call authenticateWithCompletionHandler. You can optionally pass in a block of code that will be called once the user is authenticated.

But there’s a trick. There’s another way for the user to log in (or log out!). He can be using your app, switch to the Game Center app, log or out from there, and switch back to your app.

So your app needs to know whenever the authentication status changes. You can find out about these by registering for an “authentication changed” notification.

So, our strategy to authenticate the player will be as follows:

  • Create a singleton object to keep all the Game Center code in one spot.
  • When the singleton object starts up, it will register for the “authentication changed” notification.
  • The game will call a method on the singleton object to authenticate the user.
  • Whenever the user is authentiated (or logs out), the “authentication changed” callback will be called.
  • The callback will keep track of whether the user is currently authenticated, for use later.

Now that you’re armed with this plan, let’s try it out!

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!

Landscape Orientation and GKMatchmakerViewController

You might have noticed that by default, the GKMatchmakerViewController appears in portrait orientation. Obviously, this is quite annoying since this Cocos2D game is in landscape!

Luckily, you can put in a patch for this with Objective-C categories by forcing the GKMatchmakerViewController to accept landscape-only orientations.

To do this, 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 GKMatchmakerViewController-LandscapeOnly.m, and click Finish.

Replace the contents of GKMatchmakerViewController-LandscapeOnly.h with the following:

#import <Foundation/Foundation.h>
#import <GameKit/GameKit.h>
 
@interface GKMatchmakerViewController(LandscapeOnly)
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation;
@end

Then replace the contents of GKMatchmakerViewController-LandscapeOnly.m with the following:

#import "GKMatchmakerViewController-LandscapeOnly.h"
 
@implementation GKMatchmakerViewController (LandscapeOnly)
 
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { 
    return ( UIInterfaceOrientationIsLandscape( interfaceOrientation ) );
}
 
@end

And that’s it! Compile and run your app and the view controller should show up in landscape mode right away:

GKMatchmakerViewController in Landscape Mode

Where To Go From Here?

Here is a sample project with all of the code we’ve developed so far in this Game Center tutorial.

In the second part of the tutorial series, we’ll cover how to send data back and forth between each device in the game, and wrap up the game into an exciting cat vs. kid race!

In the meantime, if you have any questions, comments, or suggestions for future tutorials, please join the forum discussion below! And don’t forget to vote for what tutorial you’d like to see next in the sidebar! :]

Ray Wenderlich

Ray is an indie software developer currently focusing on iPhone and iPad development, and the administrator of this site. He’s the founder of a small iPhone development studio called Razeware, and is passionate both about making apps and teaching others the techniques to make them.

When Ray’s not programming, he’s probably playing video games, role playing games, or board games.

User Comments

45 Comments

[ 1 , 2 , 3 ]
  • Is gamecentre popular? What if I wa t to use Facebook or just email to sign up? Then I need a server? Not sure if your other tutorial cover this. Getting confusing for me.
    esael
  • Hi,

    First of all, thanks for this excellent tutorial ;)

    Any chance to get it updated with cocos2d v2? I'm struggling with the following sentence, since no rootviewcontroller is now defined:

    Code: Select all
    AppDelegate * delegate = (AppDelegate *) [UIApplication sharedApplication].delegate;
    [[GCHelper sharedInstance] findMatchWithMinPlayers:2 maxPlayers:2 viewController:delegate.viewController delegate:self];


    If I change it to:

    Code: Select all
    AppController *app = (AppController*) [[UIApplication sharedApplication] delegate];
    UINavigationController *viewController = [app navController];
    [[GCHelper sharedInstance] findMatchWithMinPlayers:2 maxPlayers:2 viewController:viewController delegate:self];


    I get the following error:

    Code: Select all
    *** Assertion failure in -[CCDirectorDisplayLink startAnimation]
    *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'displayLink must be nil. Calling startAnimation twice?'


    Any idea? Thanks in advance!
    MaxRC
  • I answer myself:

    In GCHelper.mm, in findMatchWithMinPlayers I removed this line two times:

    Code: Select all
    [presentingViewController dismissModalViewControllerAnimated:NO];
    MaxRC
  • Hi Ray!

    The GameCenter tutorial has been REALLY helpful so far, but I've reached the point of actually displaying the matchmaker in the HelloWorldLayer, and I've gotten stuck because I'm not using Cocos2D, so I wasn't sure where to stick the code... In my game, I have a main menu screen (menuViewController) where you can select single player or multiplayer, so I figured a good time to call the [[GCHelper sharedInstance findMatchWithMinPlayers: etc] method would be on my Multiplayer Button press, but I'm not sure how to modify the call.

    I don't know if I need this line at all: AppDelegate * delegate = (AppDelegate *) [UIApplication sharedApplication].delegate;
    But if I delete it and just stick "self" in for viewController and delegate, I get errors about trying to present my own view controller. I think this must be a fairly easy adjustment, but I'm not tweaking it correctly :(

    Thanks!

    Elizabeth
    esalazar
  • I figured out my own question!

    I deleted the following line entirely: AppDelegate * delegate = (AppDelegate *) [UIApplication sharedApplication].delegate;
    As well as the addition of a viewController property to AppDelegate, and slightly altered my GCHelper sharedInstance call to this:

    [[GCHelper sharedInstance] findMatchWithMinPlayers:2 maxPlayers:2 viewController:self delegate:self];

    From my menuViewController.

    Glad it wasn't too much of a fix, or too much banging my head against the wall!

    Now on with the tutorial :)

    -Elizabeth
    esalazar
  • Edit:


    I had a problem, I couldn't connect to Sandbox GameCenter for some reason, I kinda did some research and it turns out you have to wait sometime to get apple to recognize your request.

    Kinda weird because my device would accept the Game on Sandbox mode but on the device I could:


    a)Sometimes I couldn't log in with the error: "Can't connect to Game Center"
    b)Sometimes I get this error "Game Center doesn't recognize this game"

    I was constantly like whaaaaa, I waited a day and it works like a charm...

    I guess people if you have this error, just wait it out.(Check that your code is correct).

    Cheers everyone, hope this helps someone.
    GumbiRo
  • Hi Ray, I have a curiosity want to know if in a multiplayer game players use separate cameras to focus on the player or share the same camera
    Eliax22
  • hi i have slightly changed the code adding a singleton that is the implementation of the delegate:
    Code: Select all

    #import <Foundation/Foundation.h>
    #import "GCHelper.h"
    #import "cocos2d.h"

    enum GameStatePP {
        kGameStatePlaying,
        kGameStatePaused
    };

    typedef enum {
        kGameStateWaitingForMatch = 0,
        kGameStateWaitingForRandomNumber,
        kGameStateWaitingForStart,
        kGameStateActive,
        kGameStateDone
    } GameState;

    typedef enum {
        kEndReasonWin,
        kEndReasonLose,
        kEndReasonDisconnect
    } EndReason;

    typedef enum {
        kMessageTypeRandomNumber = 0,
        kMessageTypeGameBegin,
        kMessageTypeMove,
        kMessageTypeGameOver
    } MessageType;

    typedef struct {
        MessageType messageType;
    } Message;

    typedef struct {
        Message message;
        uint32_t randomNumber;
        int idSchema;
    } MessageRandomNumber;

    typedef struct {
        Message message;
    } MessageGameBegin;

    typedef struct {
        Message message;
    } MessageMove;

    typedef struct {
        Message message;
        BOOL player1Won;
    } MessageGameOver;




    @interface MatchDelegate : NSObject <GCHelperDelegate>{
        BOOL isPlayer1;
        GameState gameState;
       
        uint32_t ourRandom, ourSchema;
        BOOL receivedRandom;
        NSString *otherPlayerID;
       
        NSString *debugString;
        CCLabelTTF *ccdebLab;
    }

    +(MatchDelegate *)sharedInstance;
    -(void)setDelegateLabel:(CCLabelTTF *)debLab;

    @property (nonatomic,assign) NSString *debugString;
    @property (nonatomic,assign) CCLabelTTF *ccdebLab;

    @end


    Code: Select all

    #import "MatchDelegate.h"
    #import "AppDelegate.h"
    #import "RootViewController.h"

    #import "cocos2d.h"
    #import "HelloWorldLayer.h"

    @implementation MatchDelegate

    @synthesize debugString,ccdebLab;

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

    -(void)setDelegateLabel:(CCLabelTTF *)debLab{
        NSLog(@"DEBUG setDelegate");
        ccdebLab=debLab;
        debugString=@"";
        [debugString retain];
        isPlayer1 = YES;

        AppDelegate *delegate = (AppDelegate *) [UIApplication sharedApplication].delegate;
        [[GCHelper sharedInstance] findMatchWithMinPlayers:2 maxPlayers:2 viewController:delegate.viewController delegate:self];
        ourRandom = arc4random();
        ourSchema = arc4random()%30;
    #if (!TARGET_IPHONE_SIMULATOR)
        ourRandom = 100;
        ourSchema=10;
    #else
        ourRandom = 10;
        ourSchema=11;
    #endif
        [self setGameState:kGameStateWaitingForMatch];
    }


    #pragma mark GCHelperDelegate

    -(void)playerActivity{
        NSLog(@"%s",__PRETTY_FUNCTION__);
    }

    - (void)matchStarted {
        NSLog(@"Match started");
        debugString=[[debugString stringByAppendingString:@"Match started\n"] retain];
        [ccdebLab setString:debugString];
        if (receivedRandom) {
            [self setGameState:kGameStateWaitingForStart];
        } else {
            [self setGameState:kGameStateWaitingForRandomNumber];
        }
        [self sendRandomNumber];
        [self tryStartGame];
    }

    - (void)matchEnded {
        NSLog(@"Match ended");
        [[GCHelper sharedInstance].match disconnect];
        [GCHelper sharedInstance].match = nil;
        [self endScene:kEndReasonDisconnect];
    }

    - (void)match:(GKMatch *)match didReceiveData:(NSData *)data fromPlayer:(NSString *)playerID {
        NSLog(@"%s",__PRETTY_FUNCTION__);
        // Store away other player ID for later
        if (otherPlayerID == nil) {
            otherPlayerID = [playerID retain];
        }
       
        Message *message = (Message *) [data bytes];
        if (message->messageType == kMessageTypeRandomNumber) {
           
            MessageRandomNumber * messageInit = (MessageRandomNumber *) [data bytes];
            NSLog(@"Received random number: %ud, ours %ud", messageInit->randomNumber, ourRandom);
    //        NSLog(@"Received schema number: %ud, ours %ud", messageInit->idSchema, ourSchema);
            debugString=[[debugString stringByAppendingFormat:@"Received random number: %ud, ours %ud\n", messageInit->randomNumber, ourRandom] retain];
            [ccdebLab setString:debugString];

            bool tie = false;
           
            if (messageInit->randomNumber == ourRandom) {
                NSLog(@"TIE!");
                tie = true;
                ourRandom = arc4random();
                [self sendRandomNumber];
            } else if (ourRandom > messageInit->randomNumber) {
                NSLog(@"We are player 1");
                debugString=[[debugString stringByAppendingFormat:@"We are player 1\n"]retain];
                [ccdebLab setString:debugString];

                isPlayer1 = YES;
                [[CCDirector sharedDirector] replaceScene:[CCTransitionFade transitionWithDuration:0.5 scene:[HelloWorldLayer scene] withColor:ccBLACK]];

            } else {
                NSLog(@"We are player 2");
                debugString=[[debugString stringByAppendingFormat:@"We are player 2\n"] retain];
                [ccdebLab setString:debugString];
                isPlayer1 = NO;
                [[CCDirector sharedDirector] replaceScene:[CCTransitionFade transitionWithDuration:0.5 scene:[HelloWorldLayer scene] withColor:ccBLACK]];
                //            [self newOnLineGame:messageInit->idSchema];
            }
           
            if (!tie) {
                receivedRandom = YES;
                if (gameState == kGameStateWaitingForRandomNumber) {
                    [self setGameState:kGameStateWaitingForStart];
                }
                [self tryStartGame];
            }
           
        } else if (message->messageType == kMessageTypeGameBegin) {
           
            [self setGameState:kGameStateActive];
           
        } else if (message->messageType == kMessageTypeMove) {
           
            NSLog(@"Received move");
           
            if (isPlayer1) {
                //[player2 moveForward];
            } else {
                //[player1 moveForward];
            }
        } else if (message->messageType == kMessageTypeGameOver) {
           
            MessageGameOver * messageGameOver = (MessageGameOver *) [data bytes];
            NSLog(@"Received game over with player 1 won: %d", messageGameOver->player1Won);
           
            if (messageGameOver->player1Won) {
                [self endScene:kEndReasonLose];
            } else {
                [self endScene:kEndReasonWin];
            }
           
        }
    }

    - (void)sendData:(NSData *)data {
        NSLog(@"sending data");
        debugString=[[debugString stringByAppendingFormat:@"sending data\n"] retain];
        [ccdebLab setString:debugString];
        NSError *error;
        BOOL success = [[GCHelper sharedInstance].match sendDataToAllPlayers:data withDataMode:GKMatchSendDataReliable error:&error];
        if (!success) {
            NSLog(@"Error sending init packet");
            [self matchEnded];
        }
    }

    - (void)sendRandomNumber {
        MessageRandomNumber message;
        message.message.messageType = kMessageTypeRandomNumber;
        message.randomNumber = ourRandom;
        message.idSchema=ourSchema;
        NSData *data = [NSData dataWithBytes:&message length:sizeof(MessageRandomNumber)];
        [self sendData:data];
    }

    - (void)sendGameBegin {
        NSLog(@"game begin");
        debugString=[[debugString stringByAppendingFormat:@"game begin\n"]retain];
        [ccdebLab setString:debugString];
        MessageGameBegin message;
        message.message.messageType = kMessageTypeGameBegin;
        NSData *data = [NSData dataWithBytes:&message length:sizeof(MessageGameBegin)];
        [self sendData:data];
    }

    - (void)setGameState:(GameState)state {
        gameState = state;
        if (gameState == kGameStateWaitingForMatch) {
            NSLog(@"Waiting for match");
            debugString=[[debugString stringByAppendingFormat:@"Waiting for match\n"]retain];
        } else if (gameState == kGameStateWaitingForRandomNumber) {
            NSLog(@"Waiting for rand #");
            debugString=[[debugString stringByAppendingFormat:@"Waiting for rand #\n"]retain];
        } else if (gameState == kGameStateWaitingForStart) {
            NSLog(@"Waiting for start");
            debugString=[[debugString stringByAppendingFormat:@"Waiting for start\n"]retain];
        } else if (gameState == kGameStateActive) {
            NSLog(@"Activeh");
            debugString=[[debugString stringByAppendingFormat:@"Activeh\n"]retain];
        } else if (gameState == kGameStateDone) {
            NSLog(@"Done");
            debugString=[[debugString stringByAppendingFormat:@"Done\n"]retain];
        }
        [ccdebLab setString:debugString];
    }

    - (void)sendMove {
        MessageMove message;
        message.message.messageType = kMessageTypeMove;
        NSData *data = [NSData dataWithBytes:&message length:sizeof(MessageMove)];
        [self sendData:data];
    }

    - (void)sendGameOver:(BOOL)player1Won {
        MessageGameOver message;
        message.message.messageType = kMessageTypeGameOver;
        message.player1Won = player1Won;
        NSData *data = [NSData dataWithBytes:&message length:sizeof(MessageGameOver)];
        [self sendData:data];
    }


    - (void)tryStartGame {
        NSLog(@"%s",__PRETTY_FUNCTION__);
        if (isPlayer1 && gameState == kGameStateWaitingForStart) {
            [self setGameState:kGameStateActive];
            [self sendGameBegin];
        }
    }

    - (void)endScene:(EndReason)endReason {
        NSLog(@"%s %d %d",__PRETTY_FUNCTION__,gameState,kGameStateDone);
       
        if (gameState == kGameStateDone) return;
        [self setGameState:kGameStateDone];
       
        NSString *message;
        if (endReason == kEndReasonWin) {
            message = @"You win!";
        } else if (endReason == kEndReasonLose) {
            message = @"You lose!";
        } else if (endReason == kEndReasonDisconnect) {
            message = @"Player Disconnected";
        }
        NSLog(message);
     
        if (isPlayer1) {
            if (endReason == kEndReasonWin) {
                NSLog(@"game over win");
                [self sendGameOver:true];
            } else if (endReason == kEndReasonLose) {
                NSLog(@"game over loose");
                [self sendGameOver:false];
            }
        }
    }

    @end


    in helloworld i put:
    Code: Select all
       
    [[MatchDelegate sharedInstance] setDelegateLabel:debugLabel];
    /*
            AppDelegate * delegate = (AppDelegate *) [UIApplication sharedApplication].delegate;
            [[GCHelper sharedInstance] findMatchWithMinPlayers:2 maxPlayers:2 viewController:delegate.viewController delegate:self];
    */


    the result: 0 player found...while the original game works perfectly...
    there is some reason?
    this is the link with the full source
    Code: Select all
    http://speedy.sh/v4tAy/CatRace.zip


    thanks
    sefiroths
  • however, sometimes, the device works till sending data, the viewcontroller buttons becames greyed and dismissing viewcontroller, in the simulator the viewcontroller buttons becames greyed but the viewcontroller does not dismiss, and in the debug label i see waiting for match...
    what could it mean?
    sefiroths
  • I would like to use Game Center to allow people to compete against each other with challenges however my game creates unique challenges every time it scrambles the puzzle and every time you select a picture for the puzzle (If you're in picture mode). So I need to be able to have a unique payload with a challenge, for instance in my game SnapWrap you can select 3 x 3 or 4 x 4 or 5 x 5 size puzzle and then it scrambles it and you have to put it back together or you can select a picture and it scrambles that and you have to put it back together. So a challenge consists of the exact scramble, the number of rows and columns and either numbers or a picture. Is there a way to either pass a payload with a challenge, or if not, can a unique URL be passed in which case I can would have to save the challenge on my own servers?
    BuddhaBandit
  • My matchmakerviewcontroller is not opening up

    - (void)findMatchWithMinPlayers:(int)minPlayers maxPlayers:(int)maxPlayers viewController:(UIViewController *)viewController delegate:(id)theDelegate {

    if (!gameCenterAvailable) return;

    matchStarted = NO;
    self.match = nil;
    self.presentingViewController = viewController;
    delegate = theDelegate;

    if (pendingInvite != nil) {

    [presentingViewController dismissModalViewControllerAnimated:NO];
    GKMatchmakerViewController *mmvc = [[[GKMatchmakerViewController alloc] initWithInvite:pendingInvite] autorelease];
    mmvc.matchmakerDelegate = self;
    [presentingViewController presentModalViewController:mmvc animated:YES];

    self.pendingInvite = nil;
    self.pendingPlayersToInvite = nil;

    } else {

    // [presentingViewController dismissModalViewControllerAnimated:NO];
    GKMatchRequest *request = [[[GKMatchRequest alloc] init] autorelease];
    request.minPlayers = minPlayers;
    request.maxPlayers = maxPlayers;
    request.playersToInvite = pendingPlayersToInvite;

    GKMatchmakerViewController *mmvc = [[[GKMatchmakerViewController alloc] initWithMatchRequest:request] autorelease];
    mmvc.matchmakerDelegate = self;

    // [presentingViewController presentModalViewController:mmvc animated:YES];
    [presentingViewController presentViewController:mmvc animated:YES completion:nil];

    self.pendingInvite = nil;
    self.pendingPlayersToInvite = nil;

    }

    }
    vivek serhrawat
  • Two suggestions man:

    * How would we implement this with our OWN server? The whole thing running in Python for example instead of Game Center. Not that I would make my own game server personally in Python, I would choose a bit lower level language and transpose it as such. But I have a feeling many users would be interested in this... And for myself it would be absolutely invaluable.

    * Secondly, how would we implement the same with UDP rather than TCP, whether our own custom built server or Game Center server? (preferably custom built but again, just the example would be nice and I could transpose it to the best of my ability). It would be really nice to see some examples in UDP for 'connecting' players, keeping their connections alive, keeping track of ping, lag, and disconnects etc, maybe some quick and dirty examples of implementing some prediction algorithms to reduce lag. Or even simpler things such as adding a player chat console allowing them to pull up a keyboard mid-game and send messages over the server that display above their heads for a few seconds.

    These tutorials are really, really, nice. Just some suggestions for more practical examples which can really help some of us get the ball rolling by seeing written examples of popular game server functionalities.
    zimvader
  • Hey,

    you should update this gamecenter tutorial to swift. :) you probably already have this planned though! there seems to be a lack of gamecenter implementation tutorials on the net.

    Thanks again for all your work!
    Tomblasta2
    Tomblasta2
  • Thanks for the great articles.
    Can you make one for Game Center for sprite kit as a complete example.
    Akshays
[ 1 , 2 , 3 ]

Other Items of Interest

Ray's Monthly Newsletter

Sign up to receive a monthly newsletter with my favorite dev links, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

Vote for Our Next Tutorial!

Every week, we alternate between Gaming and Non-Gaming tutorial votes. This week: Non-Gaming!

    Loading ... Loading ...

Last week's winner: Best iOS Animations in 2014. [Read Now]!

Suggest a Tutorial - Past Results

Hang Out With Us!

Every month, we have a free live Tech Talk - come hang out with us!


Coming up in October: Xcode 6 Tips and Tricks!

Sign Up - October

Our Books

Our Team

Tutorial Team

  • Charlie Fulton

... 49 total!

Update Team

  • Ray Fix
  • Riccardo D'Antoni

... 15 total!

Editorial Team

  • Alexis Gallagher

... 22 total!

Code Team

  • Orta Therox

... 3 total!

Translation Team

  • Heejun Han
  • Jesus Guerra

... 32 total!

Subject Matter Experts

  • Richard Casey

... 4 total!