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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
How To Make a Multiplayer iPhone Game Hosted on Your Own Server Part 2
25 mins
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!
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!
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.