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

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 […] By Ray Wenderlich.

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

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.

Contributors

Over 300 content creators. Join our team.