First things first: We just released an update of our MULTIPLAYER App called “SNOWGRE“, available for FREE on the App Store: https://itunes.apple.com/us/app/snowgre/id560450513?mt=8
Please have a look, download it for free and consider buying some extra lives and unlock the multiplayer feature to play against people from all over the world! Play agains me => my nickname is “GABEHOE” or against my co-worker Adrian => “ADIMAN84” 😀
We’d love to hear suggestions and receive feedbacks from you… and compete with you! Hahahah…
On this blog post I am going to show you how to use Game Center (Game Kit Framework) to make an amazing Multiplayer Game like SNOWGRE… well, not the whole game, but at least I will show you how to code the multiplayer stuff! 😉
Advertisement:
First, we have to diferentiate what happens in the frontend (all the stuff you “see” with your eyes) and in the backend (all the connection stuff and data that is being exchanged between the players).
So this is the flow for the Player:
- Let the user start a multiplayer session (make a fancy Button => start Multiplayer Game)
- Present the Game Center ViewController to find oponents or invite a friend – matchmaking!
- Once Game Center has found enough players for a match, we present the game scene
let’s enable Game Center for our App:
Preparations
- Login into iTunes Connect
- Click on “Manage Your Apps”
- Click on the App Icon
- Follow the steps described on the screenshot below:
Now we have enabled and activated Game Center for our Game. Let’s go to XCode…
- Add the GameKit Framework into your project => select the Project (top left of Groups & Files), select the “Build Phases” Tab on the right, expand the “Link Binary with Libraries” section and press the “+”-button. Then, select the “GameKit.framework” and click “add”.
Authenticate the player
We have now everything preparated to begin with the implementation. First we must know first a few things about authentication. Basically whenever your App starts up, we have to authenticate (or check) the player in Game Center. If the user is already authenticated, there appears a welcome message like “Welcome back!”. If the user is not authenticated, a ViewController will show up asking for a username & password. But that’s not everything… what if the user logs out and your App is running? Fortunately there is an “authentication changed” notification so that we can immediately react and invite the user to log in again.
Let’s dig into some codeezzzz…
We need a GameCenterManager singleton, that will be responsible for all Game Center action throughout our game. Make a new class, a subclass of NSObject and name it “GameCenterManager”:
GameCenterManager.h
#import <Foundation/Foundation.h> #import <GameKit/GameKit.h> @protocol GameCenterManagerDelegate - (void)matchStarted; - (void)matchEnded; - (void)match:(GKMatch *)match didReceiveData:(NSData *)data fromPlayer:(NSString *)playerID; - (void)inviteReceived; @optional - (void) processGameCenterAuth: (NSError*) error; - (void) mappedPlayerIDToPlayer: (GKPlayer*) player error: (NSError*) error; @end @interface GameCenterManager : NSObject { id delegate; BOOL userAuthenticated; UIViewController *presentingViewController; GKMatch *match; BOOL matchStarted; NSMutableDictionary *playersDict; GKInvite *pendingInvite; NSArray *pendingPlayersToInvite; } @property (retain) UIViewController *presentingViewController; @property (retain) GKMatch *match; @property (nonatomic, assign) id delegate; @property (retain) NSMutableDictionary *playersDict; @property (retain) GKInvite *pendingInvite; @property (retain) NSArray *pendingPlayersToInvite; - (void)findMatchWithMinPlayers:(int)minPlayers maxPlayers:(int)maxPlayers viewController:(UIViewController *)viewController delegate:(id)theDelegate; - (void) authenticateLocalUser; - (void) mapPlayerIDtoPlayer: (NSString*) playerID; + (BOOL) isGameCenterAvailable; @end
GameCenterManager.m
#import "GameCenterManager.h" #import <GameKit/GameKit.h> #import "RootViewController.h" @implementation GameCenterManager @synthesize delegate; @synthesize presentingViewController; @synthesize match; @synthesize playersDict; @synthesize pendingInvite; @synthesize pendingPlayersToInvite; - (id) init { self = [super init]; if(self!= NULL) { // when we init, we check if Game Center is available if([GameCenterManager isGameCenterAvailable]) { // this is very important... since we must know if the user logs in/out NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; [nc addObserver:self selector:@selector(authenticationChanged) name:GKPlayerAuthenticationDidChangeNotificationName object:nil]; } } return self; } - (void) dealloc { [super dealloc]; } - (void)authenticationChanged { if ([GKLocalPlayer localPlayer].isAuthenticated && !userAuthenticated) { userAuthenticated = TRUE; [GKMatchmaker sharedMatchmaker].inviteHandler = ^(GKInvite *acceptedInvite, NSArray *playersToInvite) { self.pendingInvite = acceptedInvite; self.pendingPlayersToInvite = playersToInvite; [delegate inviteReceived]; }; } else if (![GKLocalPlayer localPlayer].isAuthenticated && userAuthenticated) { userAuthenticated = FALSE; } } - (void) callDelegate: (SEL) selector withArg: (id) arg error: (NSError*) err { assert([NSThread isMainThread]); if([delegate respondsToSelector: selector]) { if(arg != NULL) { [delegate performSelector: selector withObject: arg withObject: err]; } else { [delegate performSelector: selector withObject: err]; } } else { NSLog(@"Missed Method"); } } - (void) callDelegateOnMainThread: (SEL) selector withArg: (id) arg error: (NSError*) err { dispatch_async(dispatch_get_main_queue(), ^(void) { [self callDelegate: selector withArg: arg error: err]; }); } + (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); } - (void) authenticateLocalUser { if([GKLocalPlayer localPlayer].authenticated == NO) { [[GKLocalPlayer localPlayer] authenticateWithCompletionHandler:^(NSError *error) { [self callDelegateOnMainThread: @selector(processGameCenterAuth:) withArg: NULL error: error]; }]; } } #pragma mark - #pragma mark find match with min players // Add new method, right after authenticateLocalUser - (void)findMatchWithMinPlayers:(int)minPlayers maxPlayers:(int)maxPlayers viewController:(UIViewController *)viewController delegate:(id<GameCenterManagerDelegate>)theDelegate { if (![GameCenterManager isGameCenterAvailable]) 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 { // with minPlayers/maxPlayers we define how many players our multiplayer // game may or must have [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]; self.pendingInvite = nil; self.pendingPlayersToInvite = nil; } } #pragma mark - #pragma mark get players info // Add new method after authenticationChanged - (void)lookupPlayers { NSLog(@"Looking up %d players...", match.playerIDs.count); [GKPlayer loadPlayersForIdentifiers:match.playerIDs withCompletionHandler:^(NSArray *players, NSError *error) { if (error != nil) { NSLog(@"Error retrieving player info: %@", error.localizedDescription); matchStarted = NO; [delegate matchEnded]; } else { // Populate players dict self.playersDict = [NSMutableDictionary dictionaryWithCapacity:players.count]; for (GKPlayer *player in players) { NSLog(@"Found player: %@", player.alias); [[RootViewController sharedInstance] setPlayer2Alias:player.alias]; [playersDict setObject:player forKey:player.playerID]; } // Notify delegate match can begin matchStarted = YES; [delegate matchStarted]; } }]; } #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); UIAlertView *resetAlert = [[[UIAlertView alloc] initWithTitle:NSLocalizedString(@"Error", @"") message:error.localizedDescription delegate:nil cancelButtonTitle:NSLocalizedString(@"OK", @"") otherButtonTitles:nil] autorelease]; [resetAlert show]; } // 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) { // Add inside matchmakerViewController:didFindMatch, right after @"Ready to start match!": [self lookupPlayers]; } } #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!"); [self lookupPlayers]; } 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]; } - (void) reloadHighScoresForCategory: (NSString*) category { GKLeaderboard* leaderBoard= [[[GKLeaderboard alloc] init] autorelease]; leaderBoard.category= category; leaderBoard.timeScope= GKLeaderboardTimeScopeAllTime; leaderBoard.range= NSMakeRange(1, 1); [leaderBoard loadScoresWithCompletionHandler: ^(NSArray *scores, NSError *error) { [self callDelegateOnMainThread: @selector(reloadScoresComplete:error:) withArg: leaderBoard error: error]; }]; } - (void) mapPlayerIDtoPlayer: (NSString*) playerID { [GKPlayer loadPlayersForIdentifiers: [NSArray arrayWithObject: playerID] withCompletionHandler:^(NSArray *playerArray, NSError *error) { GKPlayer* player= NULL; for (GKPlayer* tempPlayer in playerArray) { if([tempPlayer.playerID isEqualToString: playerID]) { player= tempPlayer; break; } } [self callDelegateOnMainThread: @selector(mappedPlayerIDToPlayer:error:) withArg: player error: error]; }]; } @end
So this is our GameCenterManager. I used basically the same code from as Apple suggests here: GameCenterManager. But removed a few methods like “submitAchievements”, “resetAchievements”, etc. since this is not need here for the scope of this blog post. The most important method for us is “findMatchWithMinPlayers”. This method is responsible for finding an oponent player in the same network or somewhere else in the Game Center network.
Another interesting method is this one:
// 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]; }
Since we define a delegate (our “RootViewController” – more about this one later on this post), our GameCenterManager sends the data to him whenever he receives data from a player (playerID). Like this we always know when a player changes position or shoots from his gun, etc… got it?
Our delegate: RootViewController
This ViewController (can be any other ViewController that you decide to be the delegate of our GameCenterManager) decides what to do with the information we get from our oponent(s). The GameCenterManager tells our RootViewController following…
- We found an oponent (or more)! His name is “player2”
- We are ready to start the match!
- Stay tuned, I will send you his positions and his states!
- If the player2 (or other players) loose connection or abort the game, or win the game, I will notify you!
OK… now we need to handle the data we get from our GameCenterManager as following: create (if not yet existing) a ViewController (subclass of UIViewController in our case – not mandatory! We have additional code in our RootViewController… that’s why) that will be the delegate of the GameCenterManager:
RootViewController.h
#import <UIKit/UIKit.h> #import <GameKit/GameKit.h> #import "GameCenterManager.h" #import "Constants.h" @interface RootViewController : UIViewController <GameCenterManagerDelegate> { GameCenterManager *gameCenterManager; BOOL userAuthenticated; NSString *oponentPlayerID; GameState gameState; NSString *player2Alias; } @property (nonatomic, retain) GameCenterManager *gameCenterManager; @property (nonatomic, retain) NSString *player2Alias; @property (readonly) GameState gameState; - (void)sendMoveWithHeroPositions:(HeroInfo *)heroInfo; - (void)sendDisconnection; - (void)sendGameOver:(BOOL)oponentHasWon; - (void)setupMultiplayerGame; + (RootViewController *)sharedInstance; @end
RootViewController.m
#import "RootViewController.h" #import "AppDelegate.h" static RootViewController *staticRootVC = nil; @implementation RootViewController @synthesize gameCenterManager; @synthesize gameState; @synthesize player2Alias; // The designated initializer. Override if you create the controller programmatically and want to perform customization that is not appropriate for viewDidLoad. - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { if ((self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil])) { // Custom initialization staticRootVC = self; if ([GameCenterManager isGameCenterAvailable]) { // init our GameCenterManager and immediately try to authenticate the user self.gameCenterManager = [[[GameCenterManager alloc] init] autorelease]; [self.gameCenterManager setDelegate:self]; [self.gameCenterManager authenticateLocalUser]; } else { // The current device does not support Game Center. Show a fancy message. } } return self; } #pragma mark - #pragma mark multiplayer stuff // this is the method that you call when the player hits the button "START MULTIPLAYER GAME" -(void)setupMultiplayerGame { [self setGameState:kGameStateWaitingForPlayersToStartMatch]; // We command our GameCenterManager to find a match for 2 players => a screen is presented // so the user can choose if he wants to invite a friend or just find anybody to play // with all over the world... // MAIN_GAME_VIEWCONTROLLER is a ViewController that is responsible to show the // GKMatchmakerViewController modally... in cocos2d this could be: [CCDirector sharedDirector] [gameCenterManager findMatchWithMinPlayers:2 maxPlayers:2 viewController:MAIN_GAME_VIEWCONTROLLER delegate:self]; } // HeroInfo is a struct that hold information like position, state, etc. about the player's Hero! // It is good practice to send structs over the net, since they are small comparing to // NSStrings, etc. we will use always structs if possible. - (void)sendMoveWithHeroPositions:(HeroInfo *)heroInfo { NSData *data = [NSData dataWithBytes:heroInfo length:sizeof(HeroInfo)*HERO_INFO_PACKAGESIZE]; [self sendData:data]; } -(void)sendDisconnection { MessageDisconnection myMessage; myMessage.message.messageType = kMessageTypeDisconnection; NSData *data = [NSData dataWithBytes:&myMessage length:sizeof(MessageDisconnection)]; [self sendData:data]; } // if we reach the goal first, send this notification to the oponent... if we did not receive // yet the same message from our oponent! - (void)sendGameOver:(BOOL)didWinTheMatch { MessageGameIsOver myMessage; myMessage.message.messageType = kMessageTypeGameIsOver; myMessage.didWinTheMatch = didWinTheMatch; NSData *data = [NSData dataWithBytes:&myMessage length:sizeof(MessageGameIsOver)]; [self sendData:data]; } // I did achieve better results with "GKMatchSendDataReliable" instead of "GKMatchSendDataUnreliable" // although "GKMatchSendDataUnreliable" would use the UDP afaik - (void)sendData:(NSData *)data { NSError *error; BOOL success = [gameCenterManager.match sendDataToAllPlayers:data withDataMode:GKMatchSendDataReliable error:&error]; if (!success) { // try again, or review your code or wait for disconnection :) } } - (void)sendGameHasBegun { MessageGameHasBegun myMessage; myMessage.message.messageType = kMessageTypeGameHasBegun; NSData *data = [NSData dataWithBytes:&myMessage length:sizeof(MessageGameHasBegun)]; [self sendData:data]; } // this method is always called twice - (void)startGame { if (gameState == kGameStateWaitingForOponentToStart) { [self setGameState:kGameStateIsReadyToStartGame]; [self sendGameHasBegun]; [self reallyStartMultiplayerSession]; } } - (void)inviteReceived { [self setupMultiplayerGame]; } - (void)matchStarted {
[self setGameState:kGameStateWaitingForOponentToStart]; [self startGame]; } - (void)matchEnded { NSLog(@"Match ended because of disconnection or anything else... check above our GameCenterManager... handle!"); } // this method receives data from all players, in our game only from one oponent - (void)match:(GKMatch *)match didReceiveData:(NSData *)data fromPlayer:(NSString *)playerID { // Imagine we have a multiplayer game with 4 players... somehow you have to differentiate // the data you receive... are we getting new positions from player2 or player3 or player4? // so the three following lines we don't need in OUR example, but I let it there as a hint // for you if you plan to make a multiplayer game with more than 2 players in total. if (oponentPlayerID == nil) { oponentPlayerID = [playerID retain]; } // there are other ways to check if we receive a game status message or a hero position // message... but this works :) if ([data length] == sizeof(HeroInfo) * HERO_INFO_PACKAGESIZE) { // in our game we have to simulate the oponent and all his actions... in our case it // is the Hero2 object. So we send the info to that object, so that we can put the // Hero2 into the right position on the screen and into the right state (stunned, // hitting, burping, etc.) HeroInfo *messageHero = (HeroInfo *)[data bytes]; [[Hero2 sharedInstance] setHeroInfo:messageHero]; } else { // we received a game state message... something happened HeroMessage *myMessage = (HeroMessage *) [data bytes]; if (myMessage->messageType == kMessageTypeGameHasBegun) { [self setGameState:kGameStateIsReadyToStartGame]; [self reallyStartMultiplayerSession]; } else if (myMessage->messageType == kMessageTypeDisconnection) { // our GameCenterManager just told us that our oponent was disconnected fro // from the match... maybe he got a phone call, or his phone ran out of batteries, // or whatever... react! You can close the match or tell the user that he has // won or just call [self setupMultiplayerGame] again... } else if (myMessage->messageType == kMessageTypeGameIsOver) { if (myMessage->oponentHasWon) { // we received the message from the oponent, that he has won the match! } else { // we received the message from the oponent, that he has lost the game // due to an unknown (yet) reason. } } } } -(void)reallyStartMultiplayerSession { // YOUR_GAME is responsible to show the game [YOUR_GAME startMultiplayerSession]; } - (void)viewDidLoad { [super viewDidLoad]; } - (NSUInteger) supportedInterfaceOrientations { //Because your app is only landscape, your view controller for the view in your // popover needs to support only landscape return UIInterfaceOrientationMaskLandscapeLeft | UIInterfaceOrientationMaskLandscapeRight; } - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation { return UIInterfaceOrientationIsLandscape(toInterfaceOrientation); } -(BOOL)shouldAutorotate { return YES; } - (void)didReceiveMemoryWarning { // Releases the view if it doesn't have a superview. [super didReceiveMemoryWarning]; // Release any cached data, images, etc that aren't in use. } - (void)viewDidUnload { [super viewDidUnload]; // Release any retained subviews of the main view. // e.g. self.myOutlet = nil; } - (void)dealloc { [super dealloc]; } +(RootViewController *)sharedInstance { return staticRootVC; } @end
HANG ON! We’re almost finished… there is another headerfile where we define our message-structures called Constants.h. Before I show you that, let me explain some functions in RootViewController first. The method – (void)setupMultiplayerGame; tells our GameCenterManager that we want to play a multiplayer game with a friend or somebody in the world. The GameCenterManager presents then a GKMatchmakerViewController modally where the user can choose between looking for an oponent nearby or worldwide. Once Game Center has found an oponent, it tells our RootViewController that we are ready to play.
OK, let’s see what we need in Constants.h…
Constants.h
// This here defines the multiplyer of size of package we send each time to our oponent... // For example in a game, you define 60fps or sometimes 30fps... so it's not wise to send // 30 or 60 times a second a Package to the oponent!!! Obviously, right...? So don't forget // to build up a buffer or some sort of technology to cope with this problem, otherwise the // hero2 will not update smoothly on your screen... #define HERO_INFO_PACKAGESIZE 3
typedef enum { kGameStateWaitingForPlayersToStartMatch = 0, kGameStateWaitingForOponentToStart, kGameStateIsReadyToStartGame } GameState; typedef enum { kMessageTypeGameHasBegun = 0, kMessageTypeGameIsOver, kMessageTypeDisconnection } MessageType; // you can define here whatever type of status you need! typedef enum { kHerostatusBlue = 0, kHerostatusStun, kHerostatusJump, kHerostatusHit } HeroStatus; typedef struct { MessageType messageType; } HeroMessage; // This is very important. In our game we send the position (x/y) and the angle of our hero // plus the status (blue, stun, jump, hit, etc... according to your needs, you can modify it) typedef struct{ MessageType messageType; int status; float x; float y; float angle; } HeroInfo; typedef struct { HeroMessage message; } MessageGameHasBegun; typedef struct { HeroMessage message; } MessageDisconnection; typedef struct { HeroMessage message; BOOL didWinTheMatch; } MessageGameIsOver;
That’s it… you see, I have shown you a nice way to set up a multiplayer game with GameCenter. You can use the code above and modify it if you want, according to your needs.
I know this tutorial was a bit kind of advanced stuff, so if you wish more explanation on specific topics mentioned above, just leave a comment.
HAPPY CODING! 🙂
Hi,
I tried to implement your code in a project but I get an error on the [self setGameState… statement. Is it possible that a section is missing from the code on your tutorial?
Thanks very much for this fine tutorial and for your help.
Michel Desjardins
Hi Michel
Did you already implement/imported Constants.h below?
Regards
Hello, I am getting the same problem and I have imported and implemented all the classes and am getting the same errors.
Hai. i tried your code. But i got error on [self setGameState:kGameStateIsReadyToStartGame]. How to solve this?
Thank you, I have been looking for some help. I will download your app.
Have you tried to copy the code from your site and paste on Xcode 5?
Man, you have some problem on your site… try that and see.
I know that, I’m sorry it’s terrible. I will update that soon 🙂
Hey,
Thanks for posting this piece of code 🙂
I’m pretty sure that I’ll be able to manage sending/receive and updating the UI with the data received, but one thing I’m not quite able to determine is when to actually start the match : when a player Connects or when didFindMatch ?
Hugo
Can you send small project for me thats will helpful
Hey that one is nice tutorial, but could u plz update for iOS 8
OK I will try to do this in the near future 🙂
I need to match 3 players, with attributes being 1 wizard and 2 warriors. How can that be done with GKMatch?