How to make a simple game with multiplayer through Game Center. Part 1: connecting Game Center to the application

Original author: Ray Wenderlich
  • Transfer
  • Tutorial


Translation of a great tutorial on integrating multiplayer using Game Center into an iOS game. The author’s site has many lessons designed to help novice game developers.

The game you are working on is very simple. This is a race in which the dog and child are participants. Tap as fast as possible to win!

This tutorial assumes that you are familiar with the basics of Cocos2D. Otherwise, I advise you to familiarize yourself with other Cocos2D lessons that you can find here .

Note:in order to check the operation of the Game Center in the application, you must be a registered iOS developer. You also need at least one physical iOS device (in order for one copy of the application to work in the simulator and the other on the device). In the end, you need at least two different Game Center accounts for testing (don’t worry, you can create them for free, only you need a different e-mail address).

Ready? Then GO!

So, let's begin

This lesson demonstrates adding a multiplayer component to a simple game. Since the creation of the logic of the game process is not included in the goal of this lesson, I prepared a blank that contains the game code without working with the network.

Download the code and run the project. You should see something like this:

image

The game is very simple, and the code is well commented. Look through it and make sure that everything is clear to you.

Turn on Game Center

At the moment you have a simple game that you can play, except that it is very boring to play it alone! It would be a lot more fun to use Game Center to invite friends to play with you or use match finder to play with random people. But before you start using Game Center, you need to do two things:

  1. Create and configure App ID;
  2. Register the application in iTunes Connect.

Let's get started.

Create and configure App ID

The first step is to create and configure an App ID. To do this, log in to the iOS Dev Center , and from there, to the iOS Provisioning Portal. Open the App IDs tab and create a new App ID for the application, just like in the screenshot:

image

The most important part is the Bundle Identifier. It must be a unique string (that is, it cannot match the one I use!) A good way is to use your domain name followed by a unique string to avoid name matches.

When you're done, click Submit. Then open the Cat Race project, select Resources \ Info.plist .in later versions of Xcode this file is located in the Supporting Files folder and is called% Project_name% -Info.plist) and change the Bundle Identifier to the one you entered in the iOS Provisioning Portal, as shown below (of course, you will enter a different value):

image

And the last thing . To avoid any troubles on the part of Xcode, follow these steps:

  • Remove all copies of the application from your device or simulator;
  • Exit the simulator if it is running;
  • Run Project \ Clean.

Congratulations, you now have the App ID for the app, and it's configured to use! Now you can register the application in iTunes Connect and enable Game Center.

Register application in iTunes Connect

The next step will be to log into iTunes Connect and register a new application.

Once you are logged into iTunes Connect, select Manage Your Applications, then click on the blue Add New App button in the upper left. Enter the application name, SKU Number and select the Bundle ID, similar to the earlier sample, as in the screenshot:

image

Click Continue and fill in the basic information about the application. Do not worry about correctness until it is so important, because you can change it later. Now you just need to contribute something (including an icon and a screenshot) to make iTunes Connect happy.

When you are done, click Save, and if everything is done as it should, then the application status will be “Prepare for Upload”, as in the screenshot:

image

Click the blue “Manage Game Center” button in the upper right, then click “Enable”. After that click "Done". That's all, Game Center is included for your application, and you can start writing code!

By the way, in the "Manage Game Center" section, you can also manage leaderboards and, of course, achievements. In this lesson, working with them will not be discussed.

Local Player Authorization: Theory

When the game starts, the first thing you need to do is authorize the local player. It is very simple. You only have to call authenticateWithCompletionHandler. If you wish, you can make sure that a certain block of code is executed immediately after user authorization.

But there is one trick. There is one more way of authorization. To do this, switch to the Game Center application, log in from there and return back to your application.

Your application should know when the authorization status changes. You can find out about this by registering for an “authentication changed” notification. So, to authorize a player you need:

  • Create a singleton object ( approx. Transl .: singleton is a design pattern that ensures that the class has only one instance and provides global access to this instance) to keep the Game Center code in one place;
  • When the singleton object is created, it will register to receive an “authentication changed” notification;
  • The game will call the singleton object method to authorize the player;
  • When a player logs in or logs out, a callback to the authentication changed function will be performed;
  • The callback function will track if the player is currently logged in.

Local Player Authorization: Implementation

In the Cat Race project, create a new file, select the iOS \ Cocoa Touch \ Objective-C class, and click Next. Enter NSObject in the Subclass field, click Next and name the new class as GCHelper, click Finish. Next, replace the contents of GCHelper.h with the following:

#import 
#import 
@interface GCHelper : NSObject {
  BOOL gameCenterAvailable;
  BOOL userAuthenticated;
}
@property (assign, readonly) BOOL gameCenterAvailable;
+ (GCHelper *)sharedInstance;
- (void)authenticateLocalUser;
@end

This code imports the GameKit header file and creates an object with two boolean variables. One of them is necessary to monitor the availability status of the Game Center, and the second to determine when the player is authorized.

It also creates: a property so that the game can report that Game Center is available; a static method to maintain uniqueness of an instance of an object; and another method to authorize a local user (which will be called when the application starts).

Next, go to GCHelper.m and add the following code inside @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, which sets the method for creating a single instance of the class. I note that there are many options for writing singleton methods, but this is the easiest way when you do not need to worry about many threads trying to initialize a singleton at the same time.

Next, add the following method immediately 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 was taken directly from Apple's Guide to Using the Game Kit . This method checks the availability of the Game Kit on a specific device. Thanks to this, the application will be able to run on iOS 4.0 or earlier (only without multiplayer).

Next, add the following code immediately after the previous 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 the availability of the Game Center, and in case of a positive result, it registers for authentication notification. It is important that the application is registered for this notification before trying to authorize the user, so it works after authorization is complete.

The authenticationChanged callback function is very simple: it checks to see if the user was authorized during the state transition, and changes the flag accordingly.

I note that for training purposes, the function can be called several times in a row to authorize or exit to make sure that the userAuthenticated flag is different from the current status, and only shows whether there have been changes after some time.

Finally, add a method to authorize the local player immediately 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, which will be mentioned later. It serves to inform the Game Kit about the need to authorize the player. I note that it does not pass the completion handler. Since you registered to receive “authentication changed” notifications, this is optional.

Okay, now GCHelper contains all the code needed to authorize a player, so now you just have to use it! Switch to AppDelegate.m and make the following changes:

// В самом начале файла
#import "GCHelper.h"
// В конце applicationDidFinishLaunching, сразу перед
// последней строкой, которая вызывает runWithScene:
[[GCHelper sharedInstance] authenticateLocalUser];

This will create a single instance (which will register for the “authentication changed” callback during initialization), then call the authenticateLocalUser method.

Almost ready! The last step is to add the Game Kit framework to the project. To do this, select the Cat Race project in the upper left of the Groups & Files tab, go to the Build Phases tab, expand “Link Binary with Libraries” and click on the “+” button. Select GameKit.framework and click Add. Change the type from Required to Optional, and your screen should look something like this:

image

You did it! Build and run the project, and if you are logged into the Game Center, you should see something like this:

image

Now that you have logged in the player, you can begin to have fun, for example, find someone to play with!

Game Center, Game Center, make me a match

There are two ways to find someone to play with through the Game Center. This is a match search programmatically or using the built-in matchmaking interface.

In this lesson, you will learn the built-in matchmaking interface. The idea is that when you want to find a match, you pass some parameters to the GKMatchRequest object, then create and display a GKMatchmakerViewController instance.

Let's see how it works. First, make some changes to CGHelper.h:

// Добавьте в начало файла
@protocol GCHelperDelegate 
- (void)matchStarted;
- (void)matchEnded;
- (void)match:(GKMatch *)match didReceiveData:(NSData *)data 
    fromPlayer:(NSString *)playerID;
@end
// Измените строчку @interface для поддержки протоколов таким образом
@interface GCHelper : NSObject  {
// Добавьте внутрь @interface
UIViewController *presentingViewController;
GKMatch *match;
BOOL matchStarted;
id  delegate;
// Добавьте после @interface
@property (retain) UIViewController *presentingViewController;
@property (retain) GKMatch *match;
@property (assign) id  delegate;
- (void)findMatchWithMinPlayers:(int)minPlayers maxPlayers:(int)maxPlayers 
    viewController:(UIViewController *)viewController 
    delegate:(id)theDelegate;

Let's quickly go through this code.

  • You define the GCHelperDelegate protocol, which is used to notify another object of important events, such as: the start of a match, the end or receipt of data. In this game, your Cocos2D layer will implement this protocol;
  • The GCHelper object implements two protocols. The first serves to ensure that the interface for creating matches can notify this object when a match is found or not. And the second - so that Game Center can notify this object when data is received or the connection status has changed;
  • New instance variables and properties are created to follow the view controller, which will display the matchmaking interface. As well as a link to the match, a variable that lets you know whether the match has started or not, and the delegate;
  • A new method is being created so that the Cocos2D layer can find someone to play with.

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

// В начале файла
@synthesize presentingViewController;
@synthesize match;
@synthesize delegate;
// Добавьте новый метод, сразу после authenticateLocalUser
- (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;               
    [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 method, which will be called by the Cocos2D layer to search for a match. If Game Center is not available, then this method does nothing.

He initializes the match as not yet started and the object of the match as nil. It saves the view controller and delegate from the outside for future use and cancels any view controller that existed before it (in case the GKMatchmakerViewController is currently displayed).

Then comes the important part. The GKMatchRequest object allows you to specify the type of match you are looking for, such as the minimum and maximum number of players. This method sets them up as they were transferred to it (for this game at least 2 players, maximum also 2 players).

Then he creates a new instance of GKMatchmakerViewController with the received request ( approx.instance of GKMatchRequest), sets the GCHelper object to the delegate and displays the passed view controller on the screen.

From this moment, control is transferred to GKMatchmakerViewController, which allows the user to find a random player and start the game. After that, some callback functions will be called, so let's add them:

#pragma mark GKMatchmakerViewControllerDelegate
// Игрок отклонил создание матча
- (void)matchmakerViewControllerWasCancelled:(GKMatchmakerViewController *)viewController {
    [presentingViewController dismissModalViewControllerAnimated:YES];
}
// Создание матча вылетело с ошибкой
- (void)matchmakerViewController:(GKMatchmakerViewController *)viewController didFailWithError:(NSError *)error {
    [presentingViewController dismissModalViewControllerAnimated:YES];
    NSLog(@"Error finding match: %@", error.localizedDescription);    
}
// Матч найден, игра должна начаться
- (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 player cancels the match search, or an error occurs, the match creation view simply closes.

And if a match is found, it will save the match object and set the GCHelper as a delegate for the match, so that it can be notified of incoming data and the change in connection status.

He also checks to see if it is time to start the match. The match object stores the number of players to find in the expectedPlayerCount variable. If it is 0, then everyone is ready to start. Now we just log it, later we will do something interesting here.

Next, add the implementation of the GKMatchDelegate callback functions:

#pragma mark GKMatchDelegate
// Объект матча получил данные от игрока
- (void)match:(GKMatch *)theMatch didReceiveData:(NSData *)data fromPlayer:(NSString *)playerID {    
    if (match != theMatch) return;
    [delegate match:theMatch didReceiveData:data fromPlayer:playerID];
}
// Состояние игрока изменилось (например, присоединился, отсоединился)
- (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;
    }                     
}
// Объект матча не смог соединиться с игроком из-за ошибки
- (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];
}
// Матч не смог начаться из-за ошибки
- (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 you data. This method simply redirects the data to the delegate (in this game it will be the Cocos2D layer), so the game engine can use it.

match: player: didChangState is used when connecting a player to check if all players are connected so that a match can start. Otherwise, if the player disconnects, the match ends and a notification is sent to the delegate.

Okay, now we have the code to create the match, let's use it in our HelloWorldLayer. Switch to HelloWorldLayer.h and make the following changes:

// Добавьте в начало файла
#import "GCHelper.h"
// Обозначим @interface как реализующий GCHelperDelegate
@interface HelloWorldLayer : CCLayer 

Now switch to HelloWorldLayer.m and make these corrections:

// Добавьте в начало файла
#import "AppDelegate.h"
#import "RootViewController.h"
// Добавьте в конец метода init, сразу после setGameState
AppDelegate * delegate = (AppDelegate *) [UIApplication sharedApplication].delegate;                
[[GCHelper sharedInstance] findMatchWithMinPlayers:2 maxPlayers:2 viewController:delegate.viewController delegate:self];
// Добавьте новые методы в конец файла
#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 the init method. It gets the RootViewController from the App Delegate because this view controller will display the view controller creating the match. Next, a method is called to search for a match by displaying the view controller to create the match. This is the method you just wrote in GCHelper.

Next come the blanks for the functions of the beginning or end of matches, which you implement later.

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

@property (nonatomic, retain) RootViewController *viewController;

Synthesize it in AppDelegate.m:

@synthesize viewController;

Done! Build and run the application, and you should see the match controller view controller appear:

image

Now launch the application on another device so that you have two running copies at the same time (for example, it can be a simulator and your iPhone).

Important: make sure that you use different Game Center accounts on each device, otherwise nothing will work.

Click “Play Now” on both devices and, after some time, the view controller for creating the match will disappear, and you should see something like this in the log:

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

Congratulations! You have created a match between two devices! Now you're on your way to creating an online game!

Landscape Orientation and GKMatchmakerViewController

You should have noticed that by default, GKMatchmakerViewController appears in the portlet orientation. Obviously, this is very annoying, because this Cocos2D game in landscape!

Fortunately, you can fix this by forcing the GKMatchmakerViewController to accept only landscape orientations. To do this, create a new file, select the iOS \ Cocoa Touch \ Objective-C class, and subclass NSObject under the name GKMatchmakerViewController-LandscapeOnly.

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

#import 
#import 
@interface GKMatchmakerViewController(LandscapeOnly)
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation;
@end

And then the contents of GKMatchmakerViewController-LandscapeOnly.m:

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

And that’s it! Build the application and run it. Now the view controller should display in landscape view:

image

What next?

Here is an example project with all the code that was covered in this tutorial.

In the second part, we will learn how to transfer data between devices, and turn the game into a wonderful cat vs baby race!

Also popular now: