Game Center Tutorial for iOS: How To Make A Simple Multiplayer Game: Part 2/2

The second part of a Game Center tutorial series that shows you how to create a simple multiplayer iPhone game. By Ray Wenderlich.

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

Adding Network Code

You have the match set up and the player names available, so now you’re on to the real meat of the project – adding the network code!

The first thing you need to do is define a few new game states based on our diagram from earlier. Open up HelloWorldLayer.h and modify your GameState structure to be the following:

typedef enum {
    kGameStateWaitingForMatch = 0,
    kGameStateWaitingForRandomNumber,
    kGameStateWaitingForStart,
    kGameStateActive,
    kGameStateDone
} GameState;

You also need to add a new reason the game can end – disconnection. So modify the EndReason enum as follows:

typedef enum {
    kEndReasonWin,
    kEndReasonLose,
    kEndReasonDisconnect
} EndReason;

Next you need to define some structures for the messages you’ll be sending back and forth. So add the following to the top of HelloWorldLayer.h:

typedef enum {
    kMessageTypeRandomNumber = 0,
    kMessageTypeGameBegin,
    kMessageTypeMove,
    kMessageTypeGameOver
} MessageType;

typedef struct {
    MessageType messageType;
} Message;

typedef struct {
    Message message;
    uint32_t randomNumber;
} MessageRandomNumber;

typedef struct {
    Message message;
} MessageGameBegin;

typedef struct {
    Message message;
} MessageMove;

typedef struct {
    Message message;
    BOOL player1Won;
} MessageGameOver;

Note that each message starts with a message type – this is so that you have a known number you can look at for each message to identify what type of message it is.

Finally add a few more instance variables to the HelloWorldLayer class:

uint32_t ourRandom;   
BOOL receivedRandom;    
NSString *otherPlayerID;

These will keep track of the random number for this device, whether we’re received the random number from the other side, and the other player’s player ID.

OK, now let’s start implementing the networking code. Switch to HelloWorldLayer.m and modify the matchStarted method as follows:

- (void)matchStarted {    
    CCLOG(@"Match started");        
    if (receivedRandom) {
        [self setGameState:kGameStateWaitingForStart];
    } else {
        [self setGameState:kGameStateWaitingForRandomNumber];
    }
    [self sendRandomNumber];
    [self tryStartGame];
}

So when the match starts up, we check to see whether we’ve already received the random number (which can happen due to timing), and set the state appropriately. Then it calls methods (which you’re about to write) to send the random number and maybe start the game.

Let’s start with sendRandomNumber. Make the following changes to HelloWorldLayer.m:

// Add at bottom of init, and comment out previous call to setGameState
ourRandom = arc4random();
[self setGameState:kGameStateWaitingForMatch];

// Add these new methods to the top of the file
- (void)sendData:(NSData *)data {
    NSError *error;
    BOOL success = [[GCHelper sharedInstance].match sendDataToAllPlayers:data withDataMode:GKMatchSendDataReliable error:&error];
    if (!success) {
        CCLOG(@"Error sending init packet");
        [self matchEnded];
    }
}

- (void)sendRandomNumber {
    
    MessageRandomNumber message;
    message.message.messageType = kMessageTypeRandomNumber;
    message.randomNumber = ourRandom;
    NSData *data = [NSData dataWithBytes:&message length:sizeof(MessageRandomNumber)];    
    [self sendData:data];
}

sendRandomNumber creates a new MessageRandomNumber structure, and sets the random number to a random number that was generated in init. It then converts the structure into an NSData to send to the other side.

sendData calls the sendDataToAllPlayers method in the GCHelper’s match object to send the packet to the other side.

Next implement the tryStartGame method you referred to earlier. Make the following changes to HelloWorldLayer.m:

// Add right after sendRandomNumber
- (void)sendGameBegin {
    
    MessageGameBegin message;
    message.message.messageType = kMessageTypeGameBegin;
    NSData *data = [NSData dataWithBytes:&message length:sizeof(MessageGameBegin)];    
    [self sendData:data];
    
}

// Add right after update method
- (void)tryStartGame {
    
    if (isPlayer1 && gameState == kGameStateWaitingForStart) {
        [self setGameState:kGameStateActive];
        [self sendGameBegin];
    }
    
}

This is quite simiple – if it’s player 1 (who has the special privilege of acting like the “server”) and the game is ready to go, it sets the game to active, and tells the other side to do the same by sending a MessageGameBegin to the other side.

OK – now for the code that handles receiving messages from the other side. Modify your match:didReceiveData:fromPlayer method to be the following:

- (void)match:(GKMatch *)match didReceiveData:(NSData *)data fromPlayer:(NSString *)playerID {
    
    // Store away other player ID for later
    if (otherPlayerID == nil) {
        otherPlayerID = [playerID retain];
    }
    
    Message *message = (Message *) [data bytes];
    if (message->messageType == kMessageTypeRandomNumber) {
        
        MessageRandomNumber * messageInit = (MessageRandomNumber *) [data bytes];
        CCLOG(@"Received random number: %ud, ours %ud", messageInit->randomNumber, ourRandom);
        bool tie = false;

        if (messageInit->randomNumber == ourRandom) {
            CCLOG(@"TIE!");
            tie = true;
            ourRandom = arc4random();
            [self sendRandomNumber];
        } else if (ourRandom > messageInit->randomNumber) {            
            CCLOG(@"We are player 1");
            isPlayer1 = YES;            
        } else {
            CCLOG(@"We are player 2");
            isPlayer1 = NO;
        }
        
        if (!tie) {
            receivedRandom = YES;    
            if (gameState == kGameStateWaitingForRandomNumber) {
                [self setGameState:kGameStateWaitingForStart];
            }
            [self tryStartGame];        
        }
        
    } else if (message->messageType == kMessageTypeGameBegin) {        
        
        [self setGameState:kGameStateActive];
        
    } else if (message->messageType == kMessageTypeMove) {     
        
        CCLOG(@"Received move");
        
        if (isPlayer1) {
            [player2 moveForward];
        } else {
            [player1 moveForward];
        }        
    } else if (message->messageType == kMessageTypeGameOver) {        
        
        MessageGameOver * messageGameOver = (MessageGameOver *) [data bytes];
        CCLOG(@"Received game over with player 1 won: %d", messageGameOver->player1Won);
        
        if (messageGameOver->player1Won) {
            [self endScene:kEndReasonLose];    
        } else {
            [self endScene:kEndReasonWin];    
        }
        
    }    
}

This method casts the incoming data as a Message structure (which always works, because we’ve set up each structure to begin with a Message structure). It can then look at the message type to see which type of message it actually is.

  • For the MessageRandomNumber case, it checks to see if it’s player 1 or player 2 based on the random number, and advances to the next state if it was waiting for the random number (and also possibly starts the game up if it’s player 1).
  • For the MessageGameBegin case, it just switches the game to active, since this means that player 1 has just sent a begin message to player 2.
  • For the MessageMove case, it moves the other player forward a bit.
  • For the MessageGameOver case, it ends the scene based on the appropriate reason.

You’re almost done! You have most of the game logic in place, you just have a few finishing tidbits to add here and there. Make the following changes to HelloWorldLayer.m next:

// Modify setGameState as follows
// Adds debug labels for extra states
- (void)setGameState:(GameState)state {
    
    gameState = state;
    if (gameState == kGameStateWaitingForMatch) {
        [debugLabel setString:@"Waiting for match"];
    } else if (gameState == kGameStateWaitingForRandomNumber) {
        [debugLabel setString:@"Waiting for rand #"];
    } else if (gameState == kGameStateWaitingForStart) {
        [debugLabel setString:@"Waiting for start"];
    } else if (gameState == kGameStateActive) {
        [debugLabel setString:@"Active"];
    } else if (gameState == kGameStateDone) {
        [debugLabel setString:@"Done"];
    } 
    
}

// Add new methods after sendGameBegin
// Adds methods to send move and game over messages
- (void)sendMove {
    
    MessageMove message;
    message.message.messageType = kMessageTypeMove;
    NSData *data = [NSData dataWithBytes:&message length:sizeof(MessageMove)];    
    [self sendData:data];
    
}

- (void)sendGameOver:(BOOL)player1Won {
    
    MessageGameOver message;
    message.message.messageType = kMessageTypeGameOver;
    message.player1Won = player1Won;
    NSData *data = [NSData dataWithBytes:&message length:sizeof(MessageGameOver)];    
    [self sendData:data];
    
}

// Add to beginning of ccTouchesBegan:withEvent
// Sends move message to other side when user taps, but only if game is active
if (gameState != kGameStateActive) return;
[self sendMove];

// Add to end of endScene:
// If the game ends and it's player 1, sends a message to the other side
if (isPlayer1) {
    if (endReason == kEndReasonWin) {
        [self sendGameOver:true];
    } else if (endReason == kEndReasonLose) {
        [self sendGameOver:false];
    }
}

// Add to beginning of update:
// Makes it so only player 1 checks for game over conditions
if (!isPlayer1) return;

// Add at bottom of matchEnded
// Disconnects match and ends level
[[GCHelper sharedInstance].match disconnect];
[GCHelper sharedInstance].match = nil;
[self endScene:kEndReasonDisconnect];

// Add inside dealloc
// Releases variable initialized earlier
[otherPlayerID release];
otherPlayerID = nil;

The above code is pretty simple (and commented to tell you what it does) so we won’t dwell on it further here.

Phew! That was a lot of code, but the good news is it’s DONE! Compile and run your code on two devices, and you should be able to have a complete race!

A multiplayer game using Game Center and Cocos2D