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

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

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 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 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 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 tutorial.

In the next 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! :]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值