Game Center Tutorial: How To Make A Simple Multiplayer Game with Sprite Kit: Part 2/2

Learn how to make a simple multiplayer racing game with Sprite Kit in this Game Center tutorial! By Ali Hafizji.

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.

Looking up Player Aliases

Switch to GameKitHelper.h and make the following changes:

// Add after @interface
@property (nonatomic, strong) NSMutableDictionary *playersDict;

This defines a property for a dictionary that will allow easy lookup of GKPlayer data (which includes the player’s alias) based on a player’s unique ID.

Then switch to GameKitHelper.m and make the following changes:

// Add new method after authenticateLocalPlayer
- (void)lookupPlayers {
    
    NSLog(@"Looking up %lu players...", (unsigned long)_match.playerIDs.count);
    
    [GKPlayer loadPlayersForIdentifiers:_match.playerIDs withCompletionHandler:^(NSArray *players, NSError *error) {
        
        if (error != nil) {
            NSLog(@"Error retrieving player info: %@", error.localizedDescription);
            _matchStarted = NO;
            [_delegate matchEnded];
        } else {
            
            // Populate players dict
            _playersDict = [NSMutableDictionary dictionaryWithCapacity:players.count];
            for (GKPlayer *player in players) {
                NSLog(@"Found player: %@", player.alias);
                [_playersDict setObject:player forKey:player.playerID];
            }
            [_playersDict setObject:[GKLocalPlayer localPlayer] forKey:[GKLocalPlayer localPlayer].playerID];
            
            // Notify delegate match can begin
            _matchStarted = YES;
            [_delegate matchStarted];
        }
    }];
}

// Add inside matchmakerViewController:didFindMatch, right after @"Ready to start match!":
[self lookupPlayers];

// Add inside match:player:didChangeState:, right after @"Ready to start match!":
[self lookupPlayers];

lookupPlayers is the main method here. This is called when the match is ready, and it looks up the info for all of the players currently in the match. It also adds the local player’s information to the dictionary.

Game Center will return a GKPlayer object for each player in the match as a result. To make things easier to access later, this code puts each GKPlayer object in a dictionary, keyed by player ID.

Finally, it marks the match as started and calls the game’s delegate so it can start the game.

Before you move onto that though, test it out! Compile and run your code on two devices, and this time when you examine your console output you should see the players looked up and the game ready to go:

2014-01-06 21:50:13.867 CatRaceStarter[787:60b] Ready to start match!
2014-01-06 21:50:13.874 CatRaceStarter[787:60b] Looking up 1 players...
2014-01-06 21:50:13.894 CatRaceStarter[787:60b] Found player: Olieh
2014-01-06 21:50:13.895 CatRaceStarter[787:60b] Match has started successfully

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 game states based on our diagram from earlier. Open up MultiplayerNetworking.m and add the following:

//Add to the top of the file
typedef NS_ENUM(NSUInteger, GameState) {
    kGameStateWaitingForMatch = 0,
    kGameStateWaitingForRandomNumber,
    kGameStateWaitingForStart,
    kGameStateActive,
    kGameStateDone
};

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 the same file:

typedef NS_ENUM(NSUInteger, MessageType) {
    kMessageTypeRandomNumber = 0,
    kMessageTypeGameBegin,
    kMessageTypeMove,
    kMessageTypeGameOver
};

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 instance variables to the implementation section of the MutliplayerNetorking class:

@implementation MultiplayerNetworking {
  uint32_t _ourRandomNumber;
  GameState _gameState;
  BOOL _isPlayer1, _receivedAllRandomNumbers;
   
  NSMutableArray *_orderOfPlayers;
};

These will keep track of the random number for the local device, whether all the random numbers have been received and the order of players in the match. The order of the players will be decided on the random number each player generates, as per our strategy.

OK, now let’s start implementing the networking code. Modify the matchStarted method and also add two stubs methods as follows:

- (void)matchStarted
{
    NSLog(@"Match has started successfully");
    if (_receivedAllRandomNumbers) {
        _gameState = kGameStateWaitingForStart;
    } else {
        _gameState = kGameStateWaitingForRandomNumber;
    }
    [self sendRandomNumber];
    [self tryStartGame];
}

- (void)sendRandomNumber
{
    
}

- (void)tryStartGame
{
    
}

matchStarted first checks if the game has received random numbers from all players of the match. If it has, then it moves the game state to the “waiting for start” state.

Before you fill out the stub methods, you need to initialize the game state variable and generate a random number. Make the following changes to MultiplayerNetworking.m:

//Add to the top of the file
#define playerIdKey @"PlayerId"
#define randomNumberKey @"randomNumber"

//Add to implementation section
- (id)init
{
    if (self = [super init]) {
        _ourRandomNumber = arc4random();
        _gameState = kGameStateWaitingForMatch;
        _orderOfPlayers = [NSMutableArray array];
        [_orderOfPlayers addObject:@{playerIdKey : [GKLocalPlayer localPlayer].playerID,
                                     randomNumberKey : @(_ourRandomNumber)}];
    }
    return self;
}

Next, add a method to send data to all member of the match.

- (void)sendData:(NSData*)data
{
    NSError *error;
    GameKitHelper *gameKitHelper = [GameKitHelper sharedGameKitHelper];
    
    BOOL success = [gameKitHelper.match
                    sendDataToAllPlayers:data
                    withDataMode:GKMatchSendDataReliable
                    error:&error];
    
    if (!success) {
        NSLog(@"Error sending data:%@", error.localizedDescription);
        [self matchEnded];
    }
}

The sendData: method uses the sendDataToAllPlayers:withDataMode:error: of the GKMatch object to send data to all players of the match. Using GKMatchSendDataReliable ensures that the data sent will be received by all other players of the game. Of Course the players need to be connected to the network at all times for this to work, else the game would just end.

Armed with the power to send messages to other players, add the following code to the sendRandomNumber method.

MessageRandomNumber message;
message.message.messageType = kMessageTypeRandomNumber;
message.randomNumber = _ourRandomNumber;
NSData *data = [NSData dataWithBytes:&message length:sizeof(MessageRandomNumber)];
[self sendData:data];

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

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

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

// Fill the contents of tryStartGame as shown
- (void)tryStartGame {
    if (_isPlayer1 && _gameState == kGameStateWaitingForStart) {
        _gameState = kGameStateActive;
        [self sendGameBegin];
    }
}

This is quite simple – 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.

Build and run on two devices and create a match as you did before. If you everything goes well you should not see any errors on the console and have the following logs printed:

2014-01-06 23:13:19.846 CatRaceStarter[814:60b] Ready to start match!
2014-01-06 23:13:19.848 CatRaceStarter[814:60b] Looking up 1 players...
2014-01-06 23:13:19.873 CatRaceStarter[814:60b] Found player: Tapstudio
2014-01-06 23:13:19.874 CatRaceStarter[814:60b] Match has started successfully

OK – now for the code that handles receiving messages from the other side. Add the following stub methods to the MultiplayerNetworking.m file.

-(void)processReceivedRandomNumber:(NSDictionary*)randomNumberDetails {
    
}

- (BOOL)isLocalPlayerPlayer1
{
  return NO;
}

You will be filling these out in just a moment. Next, modify your match:didReceiveData:fromPlayer: method to handle the random number message:

- (void)match:(GKMatch *)match didReceiveData:(NSData *)data fromPlayer:(NSString *)playerID {
    //1
    Message *message = (Message*)[data bytes];
    if (message->messageType == kMessageTypeRandomNumber) {
        MessageRandomNumber *messageRandomNumber = (MessageRandomNumber*)[data bytes];
        
        NSLog(@"Received random number:%d", messageRandomNumber->randomNumber);
        
        BOOL tie = NO;
        if (messageRandomNumber->randomNumber == _ourRandomNumber) {
            //2
            NSLog(@"Tie");
            tie = YES;
            _ourRandomNumber = arc4random();
            [self sendRandomNumber];
        } else {
            //3
            NSDictionary *dictionary = @{playerIdKey : playerID,
                                         randomNumberKey : @(messageRandomNumber->randomNumber)};
            [self processReceivedRandomNumber:dictionary];
        }
        
        //4
        if (_receivedAllRandomNumbers) {
            _isPlayer1 = [self isLocalPlayerPlayer1];
        }
        
        if (!tie && _receivedAllRandomNumbers) {
            //5
            if (_gameState == kGameStateWaitingForRandomNumber) {
                _gameState = kGameStateWaitingForStart;
            }
            [self tryStartGame];
        }
    }
}

This method casts the incoming data as a Message structure (which always works, because you’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. Let’s go through it step by step.

  1. The received data is first cast into a MessageStruct.
  2. The received number is compared with the locally generated number. In case there is a tie you regenerate the random number and send again.
  3. If the random number received is not the same as the locally generated one, the method creates a dictionary that stores the player id and the random number it generated.
  4. When all random numbers are received and the order of players has been determined the _receivedAllRandomNumbers variable will be true. In this case you check if the local player is player 1.
  5. Finally if it wasn’t a tie and the local player is player 1 you initiate the game. Else you move the game state to “waiting for start”.

You now have all the code in place to receive and send the random number message. However, you still need to process the incoming random number an arrange the players in order. The players will be arranged on the basis on the random number they generate. The one that has the highest is player 1, the one next in line is player 2 and so on. Fill the processReceivedRandomNumber: method with the following code:

//1
if([_orderOfPlayers containsObject:randomNumberDetails]) {
  [_orderOfPlayers removeObjectAtIndex:
    [_orderOfPlayers indexOfObject:randomNumberDetails]];
}
//2
[_orderOfPlayers addObject:randomNumberDetails];

//3    
NSSortDescriptor *sortByRandomNumber = 
  [NSSortDescriptor sortDescriptorWithKey:randomNumberKey   
    ascending:NO];
NSArray *sortDescriptors = @[sortByRandomNumber];
[_orderOfPlayers sortUsingDescriptors:sortDescriptors];

//4    
if ([self allRandomNumbersAreReceived]) {
  _receivedAllRandomNumbers = YES;
}

The above code is quite easy to understand so I won’t go into the details. All you’re doing here is storing the received data in an array and sorting that array based on the random number.

At this point Xcode will show an error. Thats because it can’t find the method named allRandomNumbersAreReceived. Let’s go ahead and add that in.

- (BOOL)allRandomNumbersAreReceived
{
    NSMutableArray *receivedRandomNumbers =
    [NSMutableArray array];
    
    for (NSDictionary *dict in _orderOfPlayers) {
        [receivedRandomNumbers addObject:dict[randomNumberKey]];
    }
    
    NSArray *arrayOfUniqueRandomNumbers = [[NSSet setWithArray:receivedRandomNumbers] allObjects];
    
    if (arrayOfUniqueRandomNumbers.count ==
        [GameKitHelper sharedGameKitHelper].match.playerIDs.count + 1) {
        return YES;
    }
    return NO;
}

The above method is just a helper method, it returns a boolean which is true when all random numbers have been received and are unique.

Remember the isLocalPlayerPlayer1 method you added before. Fill that out with the following lines of code:

NSDictionary *dictionary = _orderOfPlayers[0];
if ([dictionary[playerIdKey]
     isEqualToString:[GKLocalPlayer localPlayer].playerID]) {
    NSLog(@"I'm player 1");
    return YES;
}
return NO;

Build and run the game on two separate devices. You must know the drill by now. This time each device will send out a random number and one will be selected as player 1. Player 1’s device will show the following logs:

2014-01-07 00:35:10.774 CatRaceStarter[859:60b] Ready to start match!
2014-01-07 00:35:10.777 CatRaceStarter[859:60b] Looking up 1 players...
2014-01-07 00:35:10.808 CatRaceStarter[859:60b] Found player: Tapstudio
2014-01-07 00:35:10.809 CatRaceStarter[859:60b] Match has started successfully
2014-01-07 00:35:10.848 CatRaceStarter[859:60b] Received random number:-1453704186
2014-01-07 00:35:10.850 CatRaceStarter[859:60b] I'm player 1

Awesome! Your code can handle the random number message and decide the order or players. You still however need to handle the other types of messages. Append the following code to the match:didReceiveData:fromPlayer: method:

else if (message->messageType == kMessageTypeGameBegin) {
    NSLog(@"Begin game message received");
    _gameState = kGameStateActive;
} else if (message->messageType == kMessageTypeMove) {
    NSLog(@"Move message received");
    MessageMove *messageMove = (MessageMove*)[data bytes];
} else if(message->messageType == kMessageTypeGameOver) {
    NSLog(@"Game over message received");
}

Let’s pause for a moment and think about how these messages will be handled. Since the MultiplayerNetworking class is responsible for receiving and processing these messages and the GameScene is responsible for rendering the game, these two need to communicate with each other. Hence the MultiplayerNetworkingProtocol.

Add the following method to the MultiplayerNetworkingProtocol present in MultiplayerNetworking.h:

- (void)setCurrentPlayerIndex:(NSUInteger)index;

Next, switch to MultiplayerNetworking.m and add a call to the above method in the tryStartGame method:

- (void)tryStartGame {
  if (_isPlayer1 && _gameState == kGameStateWaitingForStart) {
    _gameState = kGameStateActive;
    [self sendGameBegin];
    
    //first player
    [self.delegate setCurrentPlayerIndex:0];
  }
}

Since the begin game message is only going to be sent by Player 1. The MultiplayerNetworking class can safely notify the GameScene that the local player is Player 1.

The above will work when the local player is player 1. What if the local player is not player 1, in that case you will need to find out the index of the player from the _orderOfPlayer array and notify the GameScene which player belongs to him/her.

Add the following helper methods to MultiplayerNetworking.m:

- (NSUInteger)indexForLocalPlayer
{
  NSString *playerId = [GKLocalPlayer localPlayer].playerID;
    
  return [self indexForPlayerWithId:playerId];
}

- (NSUInteger)indexForPlayerWithId:(NSString*)playerId
{
  __block NSUInteger index = -1;
  [_orderOfPlayers enumerateObjectsUsingBlock:^(NSDictionary      
    *obj, NSUInteger idx, BOOL *stop){
      NSString *pId = obj[playerIdKey];
      if ([pId isEqualToString:playerId]) {
        index = idx;
        *stop = YES;
    }
  }];
  return index;
}

The above methods simply finds the index of the local player based on their player ID. In case the local player is not Player 1, he/she will receive a begin game message. This is the right place to find of the index of the local player and inform the GameScene. Append the following code to the section in which the game begin message is handled in the match:didReceivedData:fromPlayer: method:

[self.delegate setCurrentPlayerIndex:[self indexForLocalPlayer]];

With all that in place, open GameScene.m and implement the setCurrentPlayerIndex: method:

- (void)setCurrentPlayerIndex:(NSUInteger)index {
    _currentPlayerIndex = index;
}

Build and run. Now when the game starts you’ll notice that each device is assigned a player, tap on the screen to move your player ahead. You’ll notice that as the local player moves ahead their position on the other player’s device does not change.

Player Selected

Now let’s add code to send a move message when the local player taps the screen. For this, add the following method to MultiplayerNetworking.m:

- (void)sendMove {
    MessageMove messageMove;
    messageMove.message.messageType = kMessageTypeMove;
    NSData *data = [NSData dataWithBytes:&messageMove
                                  length:sizeof(MessageMove)];
    [self sendData:data];
}

Define the above method in the MultiplayerNetworking interface in MultiplayerNetworking.h as shown below:

- (void)sendMove;

Now that you can send a move message, switch to GameScene.m and add a call to the above method in the touchesBegan:withEvent: method as shown below:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    if (_currentPlayerIndex == -1) {
        return;
    }
    [_players[_currentPlayerIndex] moveForward];
    [_networkingEngine sendMove];
}

This will handle the sending part of the equation, however you still need to add code to receive the message and take an appropriate action. For this add the following method declaration to the MultiplayerNetworkingProtocol in MultiplayerNetworking.h:

- (void)movePlayerAtIndex:(NSUInteger)index;

Next, add a call to the above method at the end of the section that handles “move messages” in the match:didReceiveData:fromPlayer:.

[self.delegate movePlayerAtIndex:[self indexForPlayerWithId:playerID]];

Open GameScene.m and add the implementation of the above method at the end of the file as shown below:

- (void)movePlayerAtIndex:(NSUInteger)index {
    [_players[index] moveForward];
}

Build and run. As you tap the screen you will notice that your player moves forward not only on your device but on your opponent’s as well.

Ali Hafizji

Contributors

Ali Hafizji

Author

Over 300 content creators. Join our team.