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

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!

Contributors

Over 300 content creators. Join our team.