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 2 of 4 of this article. Click here to view the first page.

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.

Contributors

Over 300 content creators. Join our team.