Adding Real-time Multiplayer Support to Your iOS Game

This guide shows you how to implement a real-time multiplayer game using Google Play games services in an iOS application.

Before you begin

If you haven't already done so, you might find it helpful to review the real-time multiplayer game concepts.

Before you start to code your real-time multiplayer game:

Starting a real-time multiplayer game

Your main screen is the user's primary entry point to start a real-time multiplayer game. To get users started quickly, you should present these options in the main screen of your app to the user:

  • Quick match button. Lets the user bypass the player selection UI and play against randomly selected opponents (via auto-matching). The majority of your real-time games will be created this way, so you should make this button more prominent in your main screen.

  • Invite players button. Lets the user invite friends to join a game session or specify some number of random opponents for auto-matching. When the player selects this option, your app should display either the built-in player selection user interface (UI) provided by the SDK or a custom UI for player selection.

Before your app can begin connecting players in a real-time multiplayer game, your app needs to define and create a room. The room is a virtual construct that enables network communication between multiple players in the same game session and lets players send data directly to one another. The configuration information for creating the room can come from user input provided through the player selection UI or can be provided programmatically by your app. Your app sends a room creation request to Google Play games services, which then returns a room object to your app and tracks the room and participant status during the lifecycle of the real-time multiplayer game. When an event occurs that affects the status of the room or its participants, Google Play games services notifies the relevant delegate objects in your app so that you can handle these events.

Specifying a realTimeRoomDelegate

All notifications about events affecting the room are sent to the realTimeRoomDelegate of the GPGManager object in your app. This includes events such as receiving messages from other room participants and receiving invitations to join a room via a push notification. You are strongly encouraged to assign the room delegate early in your app's lifecycle (for example, in the application:didFinishLaunchingWithOptions: method of your AppDelegate).

[GPGManager sharedInstance].realTimeRoomDelegate
        = myClassToHandleAllMultiplayerData;

Implementing a quick match

The following code shows how you might create a quick match room.

- (void)createQuickStartRoom {
    GPGMultiplayerConfig *config = [[GPGMultiplayerConfig alloc] init];
    // Could also include variants or exclusive bitmasks here
    config.minAutoMatchingPlayers = totalPlayers - 1;
    config.maxAutoMatchingPlayers = totalPlayers - 1;

    // Show waiting room UI
    [[GPGLauncherController sharedInstance] presentRealTimeWaitingRoomWithConfig:config];
}

The realTimeRoomDelegate you assigned will be informed when the room becomes active. If the user cancels out of quick-matching, your app receives a notification that the room status has been changed to deleted. You should then dismiss the view controller as described in the next section.

If your game has multiple player roles (such as farmer, archer, and wizard) and you want to restrict auto-matched games to one player of each role, add an exclusive bitmask to your room configuration. When auto-matching with this option, players will only be considered for a match when the logical AND of their exclusive bit masks is equal to 0. The following example shows how to use the bit mask to perform auto matching with three exclusive roles:

static uint64_t const ROLE_FARMER = 0x1; // 001 in binary
static uint64_t const ROLE_ARCHER = 0x2; // 010 in binary
static uint64_t const ROLE_WIZARD = 0x4; // 100 in binary

- (void)createQuickStartRoomWithRole:(uint64_t)role  {
    GPGMultiplayerConfig *config = [[GPGMultiplayerConfig alloc] init];

    // auto-match with two random auto-match opponents of different roles
    config.minAutoMatchingPlayers = 2;
    config.maxAutoMatchingPlayers = 2;
    config.exclusiveBitMask = role;

    // create room, etc.
    // …
}

Implementing player selection using the default UI

The default player selection UI in iOS displays a list of people the player might be interested in starting a game with, such as nearby players, recent opponents, and possibly people in their Google+ circles (if they signed in with Google+ with discoverable profiles). This makes it easy for the local player to find and invite friends who are ready to join a game session.

The following code shows how your app might display the default UI to let users invite specific players or random opponents.

- (void)createNormalInviteRoom {
    // 2-4 player invitation UI
    [[GPGLauncherController sharedInstance] presentRealTimeInviteWithMinPlayers:2 maxPlayers:4];
}

Implementing player selection using a custom UI

The following code shows how you might display a custom UI to let users select specific players. To implement this approach, your app needs to retrieve a list of player IDs and pass this data to a GPGRealTimeRoomMaker object.

- (void)createInvitationRoomWithPeopleListRetrievedFromCustomUI:(NSArray *)peopleList {
    GPGMultiplayerConfig *config = [[GPGMultiplayerConfig alloc] init];
    config.invitedPlayerIds = peopleList;

    // Let's not invite anybody else.
    config.minAutoMatchingPlayers = 0;
    config.maxAutoMatchingPlayers = 0;
    [GPGManager sharedInstance].realTimeRoomDelegate = myDelegateClass;
    [[GPGLauncherController sharedInstance] presentRealTimeWaitingRoomWithConfig:config];
}

Handling room events

To capture and process room events, use the GPGRealTimeRoomDelegate methods. You can use the room status reported by Google Play games services to your realTimeRoomDelegate to determine what processing actions your app should take. For example, when the prerequisite number of players join the room, Google Play games services reports that the room status changed to GPGRealTimeRoomStatusActive, indicating that your app can start the game session.

The following code shows how you might handle events in the room:didChangeStatus method.

- (void)room:(GPGRealTimeRoom *)room didChangeStatus:(GPGRealTimeRoomStatus)status {
    if (status == GPGRealTimeRoomStatusDeleted) {
        NSLog(@"RoomStatusDeleted");
        [self.lobbyDelegate multiPlayerGameWasCanceled];
        _roomToTrack = nil;
    } else if (status == GPGRealTimeRoomStatusConnecting) {
        NSLog(@"RoomStatusConnecting");
    } else if (status == GPGRealTimeRoomStatusActive) {
        NSLog(@"RoomStatusActive! Game is ready to go");
        _roomToTrack = room;

        // We may have a view controller up on screen if we're using the
        // invite UI
        [self.lobbyDelegate readyToStartMultiPlayerGame];
    } else if (status == GPGRealTimeRoomStatusAutoMatching) {
        NSLog(@"RoomStatusAutoMatching! Waiting for auto-matching to take place");
        _roomToTrack = room;
    } else if (status == GPGRealTimeRoomStatusInviting) {
        NSLog(@"RoomStatusInviting! Waiting for invites to get accepted");
    } else {
        NSLog(@"Unknown room status %ld", status);
    }
}

To capture other types of events, your app can use delegate methods such as the following:

  • room:didFailWithError. Captures error cases.
  • room:participant:didChangeStatus. Can be used to see who accepted invitations to the room and who is remaining in the game (see below for an example).
  • room:didReceiveData:fromParticipant:dataMode. Is called whenever your app receives messages from other participants in the room.

Handling invitations

When a user is sent an invitation to join a real-time multiplayer game, Google Play games services forwards the invitation to your app via push notification. To learn how to enable push notifications for your app, see Push Notifications for iOS. Notifications are sent to the realTimeRoomDelegate that you assigned.

Your app needs to handle the following scenarios:

  • The app is running when the user receives an invitation to join a game.
  • The app is not running when the user receives an invitation to join a game.

When the app is running

To handle pending invitations when the app is running, follow these steps:

  1. In the application:didReceiveRemoteNotification: method of your appDelegate, call the tryHandleRemoteNotification method of your GPGManager to handle the notification. This call passes the notification to your realTimeRoomDelegate if it detects that the notification is an invitation to join a room.

    - (void)application:(UIApplication *)application
            didReceiveRemoteNotification:(NSDictionary *)userInfo {
        NSLog(@"Received remote notification! %@", userInfo);
        if ([[GPGManager sharedInstance]
                tryHandleRemoteNotification:userInfo]) {
            // Hooray! A didReceiveRealTimeInviteForRoom method is being
            // called in our app delegate!
        } else {
            // Call other methods that might want to handle this
            // remote notification
        }
    }
    

    By using this approach, your app can still process incoming match invitations even when it goes into the background. The application:didReceiveRemoteNotification: method is called when your game returns to the foreground.

  2. Handle the incoming invitation in the didReceiveRealTimeInviteForRoom method of your realTimeRoomDelegate. How your app handles this is up to you. Try to find a balance between not distracting players when they're in the middle of a game, and ensuring that a player's invitation doesn't time out because the recipient took too long to respond.

    One approach is to display a waiting room UI with the room associated with the invitation as the only room in the list.

    - (void)didReceiveRealTimeInviteForRoom:(GPGRealTimeRoomData *)room {
        // Show waiting room UI
        NSLog(@"I received an invite from our room...");
        [[GPGLauncherController sharedInstance] presentRealTimeWaitingRoomWithRoomData:room];
    }
    

    Another (less prominent) option is to display the list of all pending invitations and prompt the user to select an invitation:

    1. Bring up an in-game 'Awaiting invitation' UI.

      - (void)didReceiveRealTimeInviteForRoom:(GPGRealTimeRoomData *)room {
          [myGameScreen showIncomingInvitationGraphic];
      }
      
    2. Bring up a list of all match invitations when the user presses a button.

      - (IBAction)viewAllIncomingInvitationsWasPressed {
          [GPGRealTimeRoomMaker listRoomsWithMaxResults:50
                  completionHandler:^(NSArray *rooms, NSError *error) {
              NSMutableArray *validRoomDataList = [NSMutableArray array];
              for (GPGRealTimeRoomData *roomData in rooms) {
                  NSLog(@"Found a room %@", roomData);
                  if (roomData.status == GPGRealTimeRoomStatusInviting) {
                      [validRoomDataList addObject:roomData];
                  }
              }
      
              // Show waiting room UI
              [[GPGLauncherController sharedInstance]
                      presentRealTimeInvitesWithRoomDataList:roomsWithInvites];
          }];
      }
      

When the app is not running ('cold start')

You also need to handle the scenario where your app is not running and the user starts your app by clicking on an invitation. This is similar to handling the previous scenario, except that your app should check for and process any incoming invitations in the application:didFinishLaunchingWithOptions: handler.

- (BOOL)application:(UIApplication *)application
        didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    self.window = [[UIWindow alloc]
            initWithFrame:[[UIScreen mainScreen] bounds]];

    // Other code is here to create your root view controller

    // You added this line in an earlier step
    [GPGManager sharedInstance].realTimeRoomDelegate
            = myClassToHandleRealTimeMethods;

    // Look to see if our application was launched from a notification
    NSDictionary *remoteNotification =
            [launchOptions objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey];

    // If one exists, see if it's a game invitation
    if (remoteNotification) {
        if ([[GPGManager sharedInstance]
                tryHandleRemoteNotification:remoteNotification]) {
        // Yes! A didReceiveRealTimeInviteForRoom method is being
        // called in our app delegate!
        } else {
            // You probably want to do other notification checking here.
        }
    }
    return YES;
}

Retrieving participant information

Participant information is encapsulated by a GPGRealTimeParticipant object. Some key properties in GPGRealTimeParticipant include:

  • displayName. This property contains either the name of the logged-in user or something like "player_29384" if the participant was auto-matched.
  • avatarUrl. This property contains either the profile picture associated with the user's account or a generic image if the participant was auto-matched.
  • participantId. This property contains a unique string that is randomly generated every time the user logs-in to play your app. The ID is not the same as the player ID associated with the logged-in user.

For information about the local player (that is, the user who is logged in to the device where your app is running), you should retrieve the GPGRealTimeRoom.localParticipant property.

For information about all participants in a room (including the local player), you can retrieve the participant list from the participants property of a GPGRealTimeRoom object. The participant's ID will appear consistent across the clients in the room. This can be useful when your real-time multiplayer game needs to determine which client should act as the 'host' of a game. For example, your game can sort the participants by participant ID and pick the player with the smallest or largest ID as the host.

You can use the room:participant:didChangeStatus method in the GPGRealTimeRoomDelegate class to be notified of changes to the status of participants in a room. This method is useful when your app wants to be informed that a player has left the room.

- (void)room:(GPGRealTimeRoom *)room
        participant:(GPGRealTimeParticipant *)participant
        didChangeStatus:(GPGRealTimeParticipantStatus)status {
    if (status == GPGRealTimeParticipantStatusLeft) {
        [self dealWithPlayerLeavingMidGame:participant];
    }
}

When participants join a room, Google Play games services attempts to create a peer-to-peer network that allows the connected participants to message each other directly. This is called the connected set. You can use the following methods and properties to retrieve information related to the participants in the connected set for the room:

  • enumerateConnectedParticipantsUsingBlock: in the GPGRealTimeRoom class. Use this method to iterate through other participants who are currently in the connected set. If you want all players invited to the match, including those not in the connected set, use the enumerateParticipantsUsingBlock method instead.
  • inConnectedSet in the GPGRealTimeRoom class. Is set to YES if the local player is in the connected set for that room.

In addition, the didChangeConnectedSet delegate method in GPGRealTimeRoomDelegate is called when the local player joins or leaves the connected set. When other participants join or drop off the connected set, this is reported by the room:participant:didChangeStatus: delegate method.

Messaging between participants

You can use the Google Play games services to broadcast data to participants that are connected to the same room and allow room participants to send messages directly to each other in your real-time multiplayer game.

Sending messages

You can send messages using either the reliable or unreliable transmission protocol supported by Google Play games services. Sending data using the reliable transmission protocol is slower, but the message is guaranteed to arrive eventually and in the correct sequence with other reliable messages. Sending data using the unreliable transmission protocol can be faster, but messages may get dropped or be received out of order.

To send message to participants, you can use the following sendData methods in the GPGRealTimeRoom class:

  • sendReliableDataToAll: and sendUnreliableDataToAll. Sends data to all players by using a reliable or unreliable transmission protocol respectively.
  • sendReliableDataToOthers: and sendUneliableDataToOthers. Sends data to all players except the local player by using a reliable or unreliable transmission protocol respectively.
  • sendReliableData:toParticipants: and sendUnreliableData:toParticipants:. Sends data only to a specific set of participants by using a reliable or unreliable transmission protocol respectively. This approach is useful if you want to minimize network bandwidth usage or when implementing a team chat system, where not all participants need this data.

When your app calls any of the sendReliable methods, the method returns an integer message ID. The same message ID is later provided by the system in the room:didSendReliableId:results:delegate callback that reports if the send operation succeeded or failed.

If you are developing a cross-platform real-time multiplayer game, make sure that your data has the correct endian format. Data stored in both Android and iOS devices are typically in little-endian format. However, the ByteBuffer object in Android defaults to the big-endian format. You should consider either setting the ByteBuffer data in the Android version of your game to use the little-endian format, or convert all of the float and integer values in the iOS version of your app from little-endian to big-endian format.

To change an integer value to a big-endian format, you can make the following call in your iOS game:

int bigEndianInt = CFSwapInt32HostToBig(nativeInt);

To change a float value to a platform-independent 'swapped' format (which is big-endian), you can make the following call in your iOS app:

CFSwappedFloat32 swapX = CFConvertFloat32HostToSwapped(x);

Receiving messages

Use the room:didReceiveData:fromParticipant:dataMode method if your app wants to handle messages that the local player receives from other participants.

- (void)room:(GPGRealTimeRoom *)room
        didReceiveData:(NSData *)data
        fromParticipant:(GPGRealTimeParticipant *)participant
        dataMode:(GPGRealTimeDataMode)dataMode {
    NSLog(@"Received data %@ from participant # %@", data,
            participant.identifier);
    // Handle this incoming data!
    ...
}

Leaving a room

To leave a room means that your app wants to disconnect an existing room from the Google Play games services servers and drop the local player from the connected set of that room. It is important to leave the room appropriately, otherwise Google Play games services will continue to send event and invitation notifications to the local player.

Your app must leave the room when one of these scenarios occurs:

  • Gameplay is over (for example, if a participant has won the game).
  • Your app is moved to the background.

When gameplay is over

To leave a room when gameplay is over, your app must call the leave method in the GPGRealTimeRoom object.

- (void)endGame {
    [self.room leave];
    [self goBackToMainMenuBecauseGameIsOver];
}

Leaving the room from room:didChangeStatus

If your game is handling room:didChangeStatus: and you want to leave the room, make sure to wrap the leave call in a dispatch_async(dispatch_get_main_queue()) block.

- (void)room:(GPGRealTimeRoom *)room didChangeStatus (GPGRealTimeRoomStatus)status {

    // Room status handling code goes here.

    if (shouldLeaveRoom) {
        dispatch_async(dispatch_get_main_queue(), ^{
            [room leave];
        });
    }
}

This prevents a known issue whereby calling the leave method from within a room:didChangeStatus method causes the system to invoke the room:didChangeStatus method again.

When going to the background

Your app must call the leave method when it enters the applicationDidEnterBackground delegate method.

- (void)applicationDidEnterBackground:(UIApplication *)application {
    // Call leave room and just safely leave the room
    if (_roomToTrack && _roomToTrack.status != GPGRealTimeRoomStatusDeleted) {
        [_roomToTrack leave];
    }
}

Alternatively, your app can also keep the multiplayer session active for a brief period before calling leave. This may be useful, for example, if your game wants to handle the scenario where a user accidentally clicks the home button during gameplay. To implement this approach, use the beginBackgroundTaskWithExpirationHandler method in the UIApplication class, and call leave only if your app is moved to the background for more than a certain amount of time (or the backgroundTimeRemaining value is approaching a lower limit). Make sure to call leave in the beginBackgroundTaskWithExpirationHandler handler to ensure correct cleanup before your app enters the suspended state. However, this approach is only recommended if you're comfortable working with background processes in iOS applications.

Send feedback about...

Play Games Services for iOS
Play Games Services for iOS