How To Make a Multiplayer iPhone Game Hosted on Your Own Server Part 2

Ray Wenderlich
Create a simple multiplayer game hosted on your own server with Game Center matchmaking!

Create a simple multiplayer game hosted on your own server with Game Center matchmaking!

This is the second part of a tutorial series that shows you how to create your own multiplayer game server and connect to it from an iPhone game – using Game Center for matchmaking!

In the first part of the series, we covered how to authenticate with Game Center, create a simple server with Python and Twisted, reading and writing data from sockets via NSInputStream and NSOutputStream, and marshalling and unmarshalling data.

In this part of the series, we’re going to wrap up this game and get it completely functional! We’ll create a match for the players and let you finally move the characters and let them race!

This tutorial picks up where the last one left off, so if you haven’t done it already, follow along with the first tutorial.

Starting a Match

At this point, Game Center has figured out a list of players that should play a game together. We need to send this to our server, so it can create a “game instance” for them to play in. We’ll just call it a match to keep things simple.

So we’ll send a “match started” message from the client to the server, telling the server the player IDs in the match. The server will set up a match object, and send a “match started” message to the clients telling them they’re good-to-go!

Let’s start with the client sending the MessageStartMatch message. Make the following changes to NetworkController.m:

// Add to end of MessageType enum
MessageStartMatch,
MessageMatchStarted,
 
// After sendPlayerConnected
- (void)sendStartMatch:(NSArray *)players {
    [self setState:NetworkStatePendingMatchStart];
 
    MessageWriter * writer = [[[MessageWriter alloc] init] autorelease];
    [writer writeByte:MessageStartMatch];
    [writer writeByte:players.count];
    for(NSString *playerId in players) {
        [writer writeString:playerId];
    }
    [self sendData:writer.data];    
}
 
// In matchmakerViewController:didFindPlayers, after the TODO
[self sendStartMatch:playerIDs];

This is pretty simple stuff. When the matchmaker finds a match, we call sendStartMatch. This marshals up the count of players, then each player Id.

Switch over to CatRaceServer.py and make the following changes:

// After MESSAGE_NOT_IN_MATCH constant
MESSAGE_START_MATCH = 2
MESSAGE_MATCH_STARTED = 3
 
MATCH_STATE_ACTIVE = 0
MATCH_STATE_GAME_OVER = 1
 
// Add before CatRacePlayer
class CatRaceMatch:
 
    def __init__(self, players):
        self.players = players
        self.state = MATCH_STATE_ACTIVE
 
    def __repr__(self):
        return "%d %s" % (self.state, str(self.players))
 
    def write(self, message):
        message.writeByte(self.state)
        message.writeByte(len(self.players))
        for matchPlayer in self.players:            
            matchPlayer.write(message)
 
// Add new method to CatRaceFactory
    def startMatch(self, playerIds):
        matchPlayers = []
        for existingPlayer in self.players:
            if existingPlayer.playerId in playerIds:
                if existingPlayer.match != None:
                    return
                matchPlayers.append(existingPlayer)
        match = CatRaceMatch(matchPlayers)
        for matchPlayer in matchPlayers:
            matchPlayer.match = match
            matchPlayer.protocol.sendMatchStarted(match)
 
// Add new methods to CatRaceProtocol
    def sendMatchStarted(self, match):
        message = MessageWriter()
        message.writeByte(MESSAGE_MATCH_STARTED)
        match.write(message)
        self.log("Sent MATCH_STARTED %s" % (str(match)))
        self.sendMessage(message)
 
    def startMatch(self, message):
        numPlayers = message.readByte()
        playerIds = []
        for i in range(0, numPlayers):
            playerId = message.readString()
            playerIds.append(playerId)
        self.log("Recv MESSAGE_START_MATCH %s" % (str(playerIds)))
        self.factory.startMatch(playerIds)
 
// Add inside processMessage after return self.playerConnected(message)
        if messageId == MESSAGE_START_MATCH:
            return self.startMatch(message)

The startMatch method is the most important part – it finds the info on each player based on their player Id, and creates a match object with all the players. For each player in the match, it sends them a “match started” message.

The other methods are just helpers to marshal/unmarshal messages or store state.

Now switch back to Xcode – we’re going to create some classes to keep track of Player and Match state. 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 Player.m, and click Save.

Replace Player.h with the following:

#import <Foundation/Foundation.h>
 
@interface Player : NSObject {
    NSString * _playerId;
    NSString * _alias;
    int _posX;
}
 
@property (retain) NSString *playerId;
@property (retain) NSString *alias;
@property  int posX;
 
- (id)initWithPlayerId:(NSString*)playerId alias:(NSString*)alias posX:(int)posX;
 
@end

This is just a plain-old object – nothing in particular to mention here. Then replace Player.m with the following:

#import "Player.h"
 
@implementation Player
@synthesize playerId = _playerId;
@synthesize alias = _alias;
@synthesize posX = _posX;
 
- (id)initWithPlayerId:(NSString*)playerId alias:(NSString*)alias posX:(int)posX 
{
    if ((self = [super init])) {
        _playerId = [playerId retain];
        _alias = [alias retain];
        _posX = posX;
    }
    return self;
}
 
- (void)dealloc
{
    [_playerId release];
    _playerId = nil;
    [_alias release];
    _alias = nil;
    [super dealloc];
}
 
@end

Now let’s repeat this to create a match object. 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 Match.m, and click Save.

Replace Match.h with the following:

#import <Foundation/Foundation.h>
 
typedef enum {
    MatchStateActive = 0,
    MatchStateGameOver
} MatchState;
 
@interface Match : NSObject {
    MatchState _state;
    NSArray * _players;
}
 
@property  MatchState state;
@property (retain) NSArray *players;
 
- (id)initWithState:(MatchState)state players:(NSArray*)players;
 
@end

And Match.m with the following:

#import "Match.h"
 
@implementation Match
@synthesize state = _state;
@synthesize players = _players;
 
- (id)initWithState:(MatchState)state players:(NSArray*)players 
{
    if ((self = [super init])) {
        _state = state;
        _players = [players retain];
    }
    return self;
}
 
- (void)dealloc
{
    [_players release];
    _players = nil;    
    [super dealloc];
}
 
@end

Now that we have these helper classes in place, switch to NetworkController.h and make the following changes:

// Add before @protocol
@class Match;
 
// Add inside @protocol
- (void)matchStarted:(Match *)match;

Here we’re adding a new method we’ll call on the delegate to notify it that a match has started.

Switch to NetworkController.m to handle the “match started” message:

// Add to top of file
#import "Match.h"
#import "Player.h"
 
// Add to end of processMessage
else if (msgType == MessageMatchStarted) {
    [self setState:NetworkStateMatchActive];
    [self dismissMatchmaker]; 
    unsigned char matchState = [reader readByte];
    NSMutableArray * players = [NSMutableArray array];
    unsigned char numPlayers = [reader readByte];
    for(unsigned char i = 0; i < numPlayers; ++i) {
        NSString *playerId = [reader readString];
        NSString *alias = [reader readString];
        int posX = [reader readInt];
        Player *player = [[[Player alloc] initWithPlayerId:playerId alias:alias posX:posX] autorelease];
        [players addObject:player];
    }
    Match * match = [[[Match alloc] initWithState:matchState players:players] autorelease];
    [_delegate matchStarted:match];
}

This simply parses the player and match data and forwards it on to the delegate.

Finally, open HelloWorldLayer.h and make the following changes:

// Add before @interface
@class Match;
 
// Add inside @interface
Match *match;
CCLabelBMFont *player1Label;
CCLabelBMFont *player2Label;
 
// Add after interface
@property (retain) Match *match;

And make the following changes to HelloWorldLayer.m:

// Add to top of file
#import "Match.h"
#import "Player.h"
 
// Add after @implementation
@synthesize match;
 
// Replace update with the following
- (void)update:(ccTime)dt {   
    player1Label.position = player1.position;
    player2Label.position = player2.position;
}
 
// Add before dealloc
- (void)matchStarted:(Match *)theMatch {
 
    self.match = theMatch;
 
    Player *p1 = [match.players objectAtIndex:0];
    Player *p2 = [match.players objectAtIndex:1];
 
    if ([p1.playerId compare:[GKLocalPlayer localPlayer].playerID] == NSOrderedSame) {
        isPlayer1 = YES;
    } else {
        isPlayer1 = NO;
    }
 
    player1.position = ccp(p1.posX, player1.position.y);
    player2.position = ccp(p2.posX, player2.position.y);
    player1.moveTarget = player1.position;
    player2.moveTarget = player2.position;
 
    if (player1Label) {
        [player1Label removeFromParentAndCleanup:YES];
        player1Label = nil;
    }
    player1Label = [CCLabelBMFont labelWithString:p1.alias fntFile:@"Arial.fnt"];
    [self addChild:player1Label];
 
    if (player2Label) {
        [player2Label removeFromParentAndCleanup:YES];
        player2Label = nil;
    }
    player2Label = [CCLabelBMFont labelWithString:p2.alias fntFile:@"Arial.fnt"];
    [self addChild:player2Label];
}

The important part here is matchstarted. It figures out whether the current player is player1 or not by seeing if the first element in the array is the current player’s playerId.

Then it sets the players to their positions based on where the server says they are. It also updates the labels based on each player’s name. The update method keeps them following the players.

Compile and run, restart your server and clients, start up a match, and finally – the match is active!

Match ready to go with your own custom server!

Moving the Players

Phew, now we can finally get to the fun part – moving the players with networking!

Like last time, let’s start with the client sending to the server.

Start by predeclaring a new method in NetworkController.h:

- (void)sendMovedSelf:(int)posX;

Then add its implementation to NetworkController.m:

// Add to MessageType enum
MessageMovedSelf,
MessagePlayerMoved,
MessageGameOver,
 
// Add after sendStartMatch
- (void)sendMovedSelf:(int)posX {
 
    MessageWriter * writer = [[[MessageWriter alloc] init] autorelease];
    [writer writeByte:MessageMovedSelf];
    [writer writeInt:posX];
    [self sendData:writer.data];
 
}

And call it in HelloWorldLayer.m by replacing ccTouchesBegan with the following:

- (void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
 
    if (match == nil || match.state != MatchStateActive) return;
 
    // Move the appropriate player forward a bit
    int posX;
    if (isPlayer1) {
        [player1 moveForward];
        posX = player1.moveTarget.x;
    } else {
        [player2 moveForward];
        posX = player2.moveTarget.x;
    }
 
    [[NetworkController sharedInstance] sendMovedSelf:posX];
 
}

Now let’s handle this on the server and send our response. Make the following changes to CatRaceServer.py:

// Add in MESSAGE constants section
MESSAGE_MOVED_SELF = 4
MESSAGE_PLAYER_MOVED = 5
MESSAGE_GAME_OVER = 6
 
PLAYER_WIN_X = 445
 
// Add new method in CatRaceMatch
    def movedSelf(self, posX, player):
        if (self.state == MATCH_STATE_GAME_OVER):
            return
        player.posX = posX
        if (player.posX >= PLAYER_WIN_X):
            self.state = MATCH_STATE_GAME_OVER
            for matchPlayer in self.players:
                if (matchPlayer.protocol):
                    matchPlayer.protocol.sendGameOver(player.match.players.index(player))
        for i in range(0, len(self.players)):
            matchPlayer = self.players[i]
            if matchPlayer != player:
                if (matchPlayer.protocol):
                    matchPlayer.protocol.sendPlayerMoved(i, posX)
 
// Add new methods to CatRaceProtocol
    def sendPlayerMoved(self, playerIndex, posX):
        message = MessageWriter()
        message.writeByte(MESSAGE_PLAYER_MOVED)
        message.writeByte(playerIndex)
        message.writeInt(posX)
        self.log("Sent PLAYER_MOVED %d %d" % (playerIndex, posX))
        self.sendMessage(message)
 
    def sendGameOver(self, winnerIndex):
        message = MessageWriter()
        message.writeByte(MESSAGE_GAME_OVER)
        message.writeByte(winnerIndex)
        self.log("Sent MESSAGE_GAME_OVER %d" % (winnerIndex))
        self.sendMessage(message)
 
    def movedSelf(self, message):
        posX = message.readInt()
        self.log("Recv MESSAGE_MOVED_SELF %d" % (posX))
        self.player.match.movedSelf(posX, self.player)
 
// Add to processMessage, right before self.log(...)
        # Match specific messages
        if (self.player == None):
            self.log("Bailing - no player set")
            return
        if (self.player.match == None):
            self.log("Bailing - no match set")
            return
        if messageId == MESSAGE_MOVED_SELF:            
            return self.movedSelf(message)

The important part here is the movedSelf method. It updates the player’s position, and checks to see if the game is over. If it is, it sends a gameOver message to everyone, otherwise it tells everyone that the player has moved and sends its new position.

Now let’s add the code to handle the replies into our client. Start with the following changes to NetworkController.h:

// Add new methods to @protocol
- (void)player:(unsigned char)playerIndex movedToPosX:(int)posX;
- (void)gameOver:(unsigned char)winnerIndex;

And the following changes to NetworkController.m:

else if (msgType == MessagePlayerMoved && _state == NetworkStateMatchActive) {
    unsigned char playerIndex = [reader readByte];
    int posX = [reader readInt];
    [_delegate player:playerIndex movedToPosX:posX];
} else if (msgType == MessageGameOver && _state == NetworkStateMatchActive) {
    unsigned char winnerIndex = [reader readByte];
    [_delegate gameOver:winnerIndex];
}

This just parses each response and forwards the info to the delegate.

Make a quick detour to PlayerSprite.h and add a new moveTo method.

- (void)moveTo:(CGPoint)moveTo;

Then switch to PlayerSprite.m, delete the existing moveTo method, and add the following:

- (void)moveTo:(CGPoint)moveTo {
 
    // Animate player if he isn't already
    if (!isMoving) {
        isMoving = YES;
        [self runAction:animateAction];
    }
 
    // Stop old move sequence
    [self stopAction:moveAction];
 
    // Figure new position to move to and create new move sequence
    moveTarget = moveTo; //ccpAdd(moveTarget, ccp(10, 0));
    CCMoveTo *moveToAction = [CCMoveTo actionWithDuration:0.5 position:moveTarget];
    CCCallFunc *callFuncAction = [CCCallFunc actionWithTarget:self selector:@selector(moveDone)];
    moveAction = [CCSequence actions:moveToAction, callFuncAction, nil];
 
    // Run new move sequence
    [self runAction:moveAction];
 
}
 
- (void)moveForward {
    CGPoint moveTo = ccpAdd(moveTarget, ccp(10, 0));
    [self moveTo:moveTo];
}

Finally switch to HelloWorldLayer.m and make the following changes:

// Add to bottom of file
- (void)player:(unsigned char)playerIndex movedToPosX:(int)posX {
 
    // We know if we receive this it's the other guy, so here's a shortcut
    if (isPlayer1) {
        [player2 moveTo:CGPointMake(posX, player2.position.y)];
    } else {
        [player1 moveTo:CGPointMake(posX, player1.position.y)];
    }
 
}
 
- (void)gameOver:(unsigned char)winnerIndex {
    match.state = MatchStateGameOver;
    if ((winnerIndex == 0 && isPlayer1) ||
        (winnerIndex != 0 && !isPlayer1)) {
        [self endScene:kEndReasonWin];
    } else {
        [self endScene:kEndReasonLose];
    }
}

This should all be pretty self explanitory.

And that’s it! Compile and run your code and finally – you have a functional networked game, hosted on your own server!

Fully functional game with Game Center on your Hosted Serve

Finishing Touches: Restarting and Continuing

Right now if you win a match and tap restart, it doesn’t actually work. That’s because we never notified the server that it should restart the match.

Also, if you shut down your device and start it back up again, the server will think you’re already in the match but won’t update you with the current match’s status.

So as some bonus finishing touches, let’s fix both of these!

As usual let’s start with the client. Predeclare a new method in NetworkController.h:

- (void)sendRestartMatch;

Implement it in NetworkController.m:

// Add new MessageType
MessageRestartMatch,
 
// Add right after sendMovedSelf
- (void)sendRestartMatch {
    MessageWriter * writer = [[[MessageWriter alloc] init] autorelease];
    [writer writeByte:MessageRestartMatch];
    [self sendData:writer.data];
}

Make some tweaks to HelloWorldLayer.h:

// Add inside @interface
CCLabelBMFont *gameOverLabel;
CCMenu *gameOverMenu;

And call the restart method from HelloWorldLayer.m (along with some fixups):

// Add at top of matchStarted
[gameOverMenu removeFromParentAndCleanup:YES];
gameOverMenu = nil;
[gameOverLabel removeFromParentAndCleanup:YES];
gameOverLabel = nil;
 
// Replace restartTapped and endScene with the following
- (void)restartTapped:(id)sender {
    [gameOverMenu removeFromParentAndCleanup:YES];
    gameOverMenu = nil;
    [gameOverLabel removeFromParentAndCleanup:YES];
    gameOverLabel = nil;
    [[NetworkController sharedInstance] sendRestartMatch];    
}
 
- (void)endScene:(EndReason)endReason {
 
    CGSize winSize = [CCDirector sharedDirector].winSize;
 
    NSString *message;
    if (endReason == kEndReasonWin) {
        message = @"You win!";
    } else if (endReason == kEndReasonLose) {
        message = @"You lose!";
    }
 
    gameOverLabel = [CCLabelBMFont labelWithString:message fntFile:@"Arial.fnt"];
    gameOverLabel.scale = 0.1;
    gameOverLabel.position = ccp(winSize.width/2, 180);
    [self addChild:gameOverLabel];
 
    CCLabelBMFont *restartLabel = [CCLabelBMFont labelWithString:@"Restart" fntFile:@"Arial.fnt"];    
 
    CCMenuItemLabel *restartItem = [CCMenuItemLabel itemWithLabel:restartLabel target:self selector:@selector(restartTapped:)];
    restartItem.scale = 0.1;
    restartItem.position = ccp(winSize.width/2, 140);
 
    gameOverMenu = [CCMenu menuWithItems:restartItem, nil];
    gameOverMenu.position = CGPointZero;
    [self addChild:gameOverMenu];
 
    [restartItem runAction:[CCScaleTo actionWithDuration:0.5 scale:1.0]];
    [gameOverLabel runAction:[CCScaleTo actionWithDuration:0.5 scale:1.0]];
 
}

Then make the following changes to CatRaceServer.py:

// Add to end of MESSAGE constants
MESSAGE_RESTART_MATCH = 7
 
// Add new method to CatRaceMatch
    def restartMatch(self, player):
        if (self.state == MATCH_STATE_ACTIVE):
            return
        self.state = MATCH_STATE_ACTIVE
        for matchPlayer in self.players:
            matchPlayer.posX = 25
        for matchPlayer in self.players:
            if (matchPlayer.protocol):
                matchPlayer.protocol.sendMatchStarted(self) 
 
// In CatRaceFactory's playerConnected, after TODO
                    if (continueMatch):
                        existingPlayer.protocol.sendMatchStarted(existingPlayer.match) 
                    else:
                        print "TODO: Quit match"
 
// Add new method to CatRaceProtocol
    def restartMatch(self, message):
        self.log("Recv MESSAGE_RESTART_MATCH")
        self.player.match.restartMatch(self.player)
 
// Add to CatRaceProtocol's processMessage, right before self.log
        if messageId == MESSAGE_RESTART_MATCH:
            return self.restartMatch(message)

Compile your code, re-run it on your devices and restart your server, and you’ll see now you can win the game, restart and keep playing!

Persistent matches across disconnection

And even cooler, if you disconnect your device (by shutting down the app and restarting) – it will pick up your match where you left off! This is great for mobile devices with intermittent connections.

Finishing Touches: Match Ending

Connecting to existing matches is cool, but not so cool if your partner leaves for good – then you’re stuck with a ghost forever!

It would be better if we let the user stick around for a certain amount of time, then cancelled the match if he hasn’t returned by then.

To do this, make the following changes to CatRaceServer.py:

// Add to top of file
from twisted.internet.task import LoopingCall
from time import time
 
// Add in constants section
SECS_FOR_SHUTDOWN = 30
 
// Add to bottom of __init__ in CatRaceMatch
        self.pendingShutdown = False
        self.shutdownTime = 0
        self.timer = LoopingCall(self.update)
        self.timer.start(5)
 
// Add new methods to CatRaceMatch
    def update(self):
        print "Match update: %s" % (str(self))
        if (self.pendingShutdown):
            cancelShutdown = True
            for player in self.players:
                if player.protocol == None:
                    cancelShutdown  =False
            if (time() > self.shutdownTime):
                print "Time elapsed, shutting down match"
                self.quit()
        else:
            for player in self.players:
                if player.protocol == None:
                    print "Player %s disconnected, scheduling shutdown" % (player.alias)
                    self.pendingShutdown = True
                    self.shutdownTime = time() + SECS_FOR_SHUTDOWN
 
    def quit(self):
        self.timer.stop()
        for matchPlayer in self.players:
            matchPlayer.match = None
            if matchPlayer.protocol:
                matchPlayer.protocol.sendNotInMatch()

In Twisted, you can schedule a method to be called every so often by using the LoopingCall method. Here we set it up to call our update method every five seconds.

In the update method, we look to see if there’s a pending shutdown. If there is, if all of the players are connected (the protocol isn’t None), we cancel the shutdown. If it’s time to shut down, we call a quit method.

If there isn’t a pending shutdown, we check to see if there should be by checking to see if any players have disconnected.

The quit method just sends the “not in match” message to all clients.

Re-run your server, start up a match, then disconnect one of the clients. The other match will keep going for a while, but then the server will shut it down as desired!

Finishing Touches: Handling Game Center Invites

Our app is starting to work pretty well, but right now our game doesn’t support Game Center invites. This is a pretty important feature to have, and pretty easy to do, so let’s look into how we can do this with hosted matches.

It works very similarly to the normal way you handle invites with Game Center, except for one important change – if you get invited to a match, you have to send a mesasge to the person who invited you that you’re connected to the server and ready to go, so it can update the Matchmaker GUI. If you don’t do this, it will show a spinning animation like it’s still waiting for you to join.

Let’s see how this works. Make the following changes to NetworkController.h:

// Add inside @interface
GKInvite *_pendingInvite;
NSArray *_pendingPlayersToInvite;
 
// Add after @interface
@property (retain) GKInvite *pendingInvite;
@property (retain) NSArray *pendingPlayersToInvite;

Here we keep instance variables/properties with the invite information that will be passed to us from Game Center.

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

// Add new MessageType
MessageNotifyReady
 
// Add to @synthesize section
@synthesize pendingInvite = _pendingInvite;
@synthesize pendingPlayersToInvite = _pendingPlayersToInvite;
 
// Add before processMessage
- (void)sendNotifyReady:(NSString *)inviter {
    MessageWriter * writer = [[[MessageWriter alloc] init] autorelease];
    [writer writeByte:MessageNotifyReady];
    [writer writeString:inviter];
    [self sendData:writer.data];
}
 
// Add at end of processMessage
else if (msgType == MessageNotifyReady) {
    NSString *playerId = [reader readString];
    NSLog(@"Player %@ ready", playerId);
    if (_mmvc != nil) {
        [_mmvc setHostedPlayerReady:playerId];
    }
}
 
// Inside inputStreamHandleEvent, replace [self sendPlayerConnected:...] with this:
BOOL continueMatch = _pendingInvite == nil;
[self sendPlayerConnected:continueMatch];
 
// Inside outputStreamHandleEvent, replace [self sendPlayerConnected:...] with this:
BOOL continueMatch = _pendingInvite == nil;
[self sendPlayerConnected:continueMatch];
 
// Inside authenticationChanged, after _userAuthenticated = TRUE
[GKMatchmaker sharedMatchmaker].inviteHandler = ^(GKInvite *acceptedInvite, NSArray *playersToInvite) {            
    NSLog(@"Received invite");
    self.pendingInvite = acceptedInvite;
    self.pendingPlayersToInvite = playersToInvite;
 
    if (_state >= NetworkStateConnected) {
        [self setState:NetworkStateReceivedMatchStatus];
        [_delegate setNotInMatch];
    }
 
};
 
// Inside findMatchWithMinPlayers:maxPlayers:viewController, replace if (FALSE) { } with the following
if (_pendingInvite != nil) {
 
    [self sendNotifyReady:_pendingInvite.inviter];
 
    self.mmvc = [[[GKMatchmakerViewController alloc] initWithInvite:_pendingInvite] autorelease];
    _mmvc.hosted = YES;
    _mmvc.matchmakerDelegate = self;
 
    [_presentingViewController presentModalViewController:_mmvc animated:YES];
    self.pendingInvite = nil;
    self.pendingPlayersToInvite = nil;
 
}

This is all pretty simple, here’s how it works:

  1. After the user is authenticated, we register an invite handler. This can be called at any time. When it is, we store away the invite info, and set ourselves as “not in the match” if we’re connected (which will call findMatchWithMinPlayers:…).
  2. In findMatchWithMinPlayers, if we have pending invite info squirreled away, we use that. But we also tell the person who invited us that we’re connected via sendNotifyReady.
  3. Upon receiving a MessageNotifyReady, we call the setHostedPlayerReady method on the GKMatchmakerViewController. This is the thing you need to call to avoid the spinning circle of death.

That’s all we need for the client-side – onto the server side! Make the following changes to CatRaceServer.py:

// Add new message constant
MESSAGE_NOTIFY_READY = 8
 
// Add new method to CatRaceFactory
    def notifyReady(self, player, inviter):
        for existingPlayer in self.players:
            if existingPlayer.playerId == inviter:
                existingPlayer.protocol.sendNotifyReady(player.playerId)
 
// In CatRaceFactory, replace print "TODO: Quit match" with the following
                        print "Quitting match!"
                        existingPlayer.match.quit()
 
// Add new methods to CatRaceProtocol
    def sendNotifyReady(self, playerId):
        message = MessageWriter()
        message.writeByte(MESSAGE_NOTIFY_READY)
        message.writeString(playerId)
        self.log("Sent PLAYER_NOTIFY_READY %s" % (playerId))
        self.sendMessage(message)
 
    def notifyReady(self, message):
        inviter = message.readString()
        self.log("Recv MESSAGE_NOTIFY_READY %s" % (inviter))
        self.factory.notifyReady(self.player, inviter)
 
// Add new case to processMessage, right before Match specific messages
        if messageId == MESSAGE_NOTIFY_READY:
            return self.notifyReady(message)

This is pretty simple stuff – when the server receives a notify ready message it just forwards it on to the appropriate dude.

Try out the code on two devices (important: you actually need two devices, the simulator does not support invites!), and you should be able to invite your other account to the match!

Handling Game Center Invites when hosted on your own server

Is Game Center Worth It?

Like I mentioned earlier in this tutorial, Game Center is nice because you don’t have to create your own account system, users don’t have to create accounts, they can use their own friends list, etc.

But Game Center isn’t always the best choice for a game where you have your own server. Here’s some reasons why you might not want to use it, and create your own user accounts instead:

  • By managing your own accounts, you have more knowledge about your customers. You can possibly get them to sign up for your mailing list, etc, instead of just having a random playerId.
  • If you use Game Center, this means you can only play with people who have Game Center accounts. One of the benefits of making your own server is you could make a cross-platform game.
  • Game Center doesn’t seem to work too well for invites after a game has already started. It is supported (sort of) but there’s no GUI element to it.

I guess what I’m trying to say is if you’re going through all the effort to make your own server, it wouldn’t be too much more work to make your own account system too, and it’s something that might be beneficial to your game in the long run.

Where To Go From Here?

Here is a sample project with all of the code from the above tutorial.

Thank you to all who voted for this tutorial in the weekly tutorial vote, I hope you enjoyed! Keep voting – a new tutorial poll goes up every week!

As I was working on this tutorial, I have a sneaking suspicion that I may have made some subtle errors in the network states and may have missed some corner cases, or maybe even got the abstraction slightly wrong. This stuff is hard to get right!

If you’re looking at this and notice any issues, or have suggestions on how to do things better, please let me know, I’m trying to learn the best way to do this stuff myself :]

If you have any questions, comments, or suggestions, please join the forum discussion below!

Ray Wenderlich

Ray is an indie software developer currently focusing on iPhone and iPad development, and the administrator of this site. He’s the founder of a small iPhone development studio called Razeware, and is passionate both about making apps and teaching others the techniques to make them.

When Ray’s not programming, he’s probably playing video games, role playing games, or board games.

User Comments

17 Comments

[ 1 , 2 ]
  • I Thank You For Sharing Multiplayer iPhone Game help
    I Will Not Soon Forget
    You're One Of The Nicest People and Best Website I Have Ever Met.....
    Thank you
    logangibs
  • Great tutorial. However, I think it needs one final touch. That is how to place and launch the server file(s) on a hosting site. You are so detailed and do such a great job on your tutorials. It leaves very little to understand, and that is great. However, personally, while I can easily run the server on my local computer real easily and it works, I would like to know how you placed it on your hosting site and any deployment instructions that need to be addressed. You gave me the cake. Now let me eat it too. :-).
    MarkyD
[ 1 , 2 ]

Other Items of Interest

Ray's Monthly Newsletter

Sign up to receive a monthly newsletter with my favorite dev links, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

Hang Out With Us!

Every month, we have a free live Tech Talk - come hang out with us!


Coming up in May: Procedural Level Generation in Games with Kim Pedersen.

Sign Up - May

Coming up in June: WWDC Keynote - Podcasters React! with the podcasting team.

Sign Up - June

Vote For Our Next Book!

Help us choose the topic for our next book we write! (Choose up to three topics.)

    Loading ... Loading ...

Our Books

Our Team

Tutorial Team

  • Marcelo Fabri

... 55 total!

Editorial Team

... 22 total!

Code Team

  • Orta Therox

... 1 total!

Translation Team

  • Victor Grushevskiy
  • Jiyeon Seo

... 38 total!

Subject Matter Experts

  • Richard Casey

... 4 total!