Game Center Tutorial: How To Make A Simple Multiplayer Game with Sprite Kit: Part 1/2

Learn how to make a simple multiplayer racing game with Sprite Kit in this Game Center tutorial! By Ali Hafizji.

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

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, you’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 GameKitHelper.h:

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

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

// Add after @interface
@property (nonatomic, strong) GKMatch *match;
@property (nonatomic, assign) id <GameKitHelperDelegate> delegate;

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

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

  • You define a protocol called GameKitHelperDelegate that you’ll use to notify other objects of when important events happen, such as the match starting, ending, or receiving data from the other party. For now, the GameViewController will be implementing this protocol.
  • The GameKitHelper 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 a new method that the GameViewController will call to look for someone to play with.

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

// Add a new private variable to the implementation section
BOOL _matchStarted;

// Add new method, right after authenticateLocalUser
- (void)findMatchWithMinPlayers:(int)minPlayers maxPlayers:(int)maxPlayers
                 viewController:(UIViewController *)viewController
                       delegate:(id<GameKitHelperDelegate>)delegate {
    
    if (!_enableGameCenter) return;
    
    _matchStarted = NO;
    self.match = nil;
    _delegate = delegate;
    [viewController dismissViewControllerAnimated:NO completion:nil];
    
    GKMatchRequest *request = [[GKMatchRequest alloc] init];
    request.minPlayers = minPlayers;
    request.maxPlayers = maxPlayers;
    
    GKMatchmakerViewController *mmvc =
        [[GKMatchmakerViewController alloc] initWithMatchRequest:request];
    mmvc.matchmakerDelegate = self;
    
    [viewController presentViewController:mmvc animated:YES completion:nil];
}

This is the method that the view controller 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 delegate for later use, and dismisses any previously existing 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 GameKitHelper 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:

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

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

// A peer-to-peer match has been found, the game should start
- (void)matchmakerViewController:(GKMatchmakerViewController *)viewController didFindMatch:(GKMatch *)match {
    [viewController dismissViewControllerAnimated:YES completion:nil];
    self.match = match;
    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 GameKitHelper 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 you’re just going to log that out – later on you’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 *)match didReceiveData:(NSData *)data fromPlayer:(NSString *)playerID {
    if (_match != match) return;
    
    [_delegate match:match didReceiveData:data fromPlayer:playerID];
}

// The player state changed (eg. connected or disconnected)
- (void)match:(GKMatch *)match player:(NSString *)playerID didChangeState:(GKPlayerConnectionState)state {
    if (_match != match) return;
    
    switch (state) {
        case GKPlayerStateConnected:
            // handle a new player connection.
            NSLog(@"Player connected!");
            
            if (!_matchStarted && match.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 *)match connectionWithPlayerFailed:(NSString *)playerID withError:(NSError *)error {
    
    if (_match != match) 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 *)match didFailWithError:(NSError *)error {
    
    if (_match != match) return;
    
    NSLog(@"Match failed with error: %@", error.localizedDescription);
    _matchStarted = NO;
    [_delegate matchEnded];
}

match:didReceiveData:fromPlayer: method is called when another player sends data to you. This method simply forwards the data onto the delegate, so that it can do the game-specific stuff with it.

For match:player:didChangeState:, 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 you have this code to establish a match, let’s use it in our GameViewController. For the matchmaker view controller to show up it is necessary that the local player is authenticated and since authenticating a local player is an asynchronous process the GameViewController needs to be notified in some way when the user is authenticated. To do this you’re going to use good old notifications. Still in GameKitHelper.m, make the following changes:

// Add to the top of the file
NSString *const LocalPlayerIsAuthenticated = @"local_player_authenticated";

// Add this between the 1st and 2nd step of authenticateLocalPlayer
if (localPlayer.isAuthenticated) {
    [[NSNotificationCenter defaultCenter] postNotificationName:LocalPlayerIsAuthenticated object:nil];
    return;
}

// Modify the 5th step of authenticateLocalPlayer
else if([GKLocalPlayer localPlayer].isAuthenticated) {
    //5
    _enableGameCenter = YES;
    [[NSNotificationCenter defaultCenter] postNotificationName:LocalPlayerIsAuthenticated object:nil];
}

Next, switch to GameKitHelper.h and add the following to the top of the file:

extern NSString *const LocalPlayerIsAuthenticated;

With that in place switch to GameViewController.m and make the following changes:

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

// Mark the GameViewController to implement GameKitHelperDelegate
@interface GameViewController()<GameKitHelperDelegate>

@end

// Add to the implementation section

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playerAuthenticated)
                                                 name:LocalPlayerIsAuthenticated object:nil];
}

- (void)playerAuthenticated {
    [[GameKitHelper sharedGameKitHelper] findMatchWithMinPlayers:2 maxPlayers:2 viewController:self delegate:self];
}

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

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

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

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

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

The most important part here is in the playerAuthenticated method. It calls the new method you just wrote on GameKitHelper 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.

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

GKMatchmakerViewController

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 match maker view controller should go away, and you should see something like this in your console log:

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!

Ali Hafizji

Contributors

Ali Hafizji

Author

Over 300 content creators. Join our team.