现在的位置: 首页 > 综合 > 正文

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

2017年10月31日 ⁄ 综合 ⁄ 共 20958字 ⁄ 字号 评论关闭

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!
:]

抱歉!评论已关闭.