Saturday 29 December 2012

GKHelper: getting GKPlayer data

I'm going to start going over useful methods from my code in order to help people with similar problems, and give myself a form of self assessment. The first method I'm going to go over is the method I use to get GKPlayer objects from player IDs. This code is from my forthcoming turn based Game Center game (name TBD).

For all my Game Center interactions, I have created a singleton manager, named GKHelper. It handles creating the async calls to Game Center, and processes the information when it comes back. It also has several forms of caching to reduce web requests.

GKHelper has a dictionary which it uses to store GKPlayers by playerID. Whenever a GKPlayer object is fetched, it is first stored in this dictionary before being returned. Whenever a GKPlayer is requested via playerID, the dictionary is checked before the request is made. This logic is handled via the following method:


- (GKPlayer*)playerForPlayerID:(NSString*)playerID {
   if(playerID == nil || [playerID isEqualToString:@""]) {
      return nil;
   }
   if(self.playersDict == nil) {
      self.playersDict = [NSMutableDictionary dictionary];
   }
   
   if([self.playersDict objectForKey:playerID]) {
      return [self.playersDict objectForKey:playerID];
   } else {
      [GKPlayer loadPlayersForIdentifiers:[NSArray arrayWithObject:playerID] withCompletionHandler:^(NSArray *players, NSError *error) {
         
         if (error != nil) {
            NSLog(@"Error retrieving player info: %@", error.localizedDescription);
         } else {
            // Populate players dict
            for (GKPlayer *player in players) {
               [self.playersDict setObject:player forKey:player.playerID];
            }
            [self.delegate performSelectorOnMainThread:@selector(playersLoaded) withObject:nil waitUntilDone:NO];
         }
      }];
   }
   return nil;
}


First, the method checks the playerID to make sure it is valid. Then, it checks the dictionary for the specified player. On the case that it cannot find the player, it begins the async web request for the player data, which it then stores upon return. The delegate is then informed on the main thread (since it may return on a background thread) that the players list has been updated (the delegate usually reloads the data in the tableview). After the web request is sent, the function returns nil indicating the player could not be found. The function will get called a second time upon receiving the player info, and the correct information will be retrieved. This function is costly, however, since the async request takes an array of playerIDs, and we're only passing in a single ID.

That's where the next two functions come in.


- (void)getPlayersForMatch:(GKTurnBasedMatch*)match {
   
   NSMutableArray *identifiers = [NSMutableArray array];
   
   for(GKTurnBasedParticipant *participant in match.participants) {
      if(participant.playerID == nil) {
         continue;
      }
      [identifiers addObject:participant.playerID];
   }
   
   [GKPlayer loadPlayersForIdentifiers:identifiers withCompletionHandler:^(NSArray *players, NSError *error) {
      
      if (error != nil) {
         NSLog(@"Error retrieving player info: %@", error.localizedDescription);
      } else {
         // Populate players dict
         for (GKPlayer *player in players) {
            [self.playersDict setObject:player forKey:player.playerID];
         }
         [self.delegate playersLoaded];
      }
   }];
}

- (void)getPlayersForMatches:(NSArray*)matches {
   NSMutableArray *identifiers = [NSMutableArray array];
   
   for(GKTurnBasedMatch *match in matches) {
      for(GKTurnBasedParticipant *participant in match.participants) {
         if(participant.playerID == nil) {
            continue;
         }
         [identifiers addObject:participant.playerID];
      }
   }
   
   [GKPlayer loadPlayersForIdentifiers:identifiers withCompletionHandler:^(NSArray *players, NSError *error) {
      
      if (error != nil) {
         NSLog(@"Error retrieving player info: %@", error.localizedDescription);
      } else {
         // Populate players dict
         for (GKPlayer *player in players) {
            [self.playersDict setObject:player forKey:player.playerID];
         }
         [self.delegate playersLoaded];
      }
   }];
}


These two functions take a GKTurnBasedMatch, and simply fill up the player dictionary with the GKPlayer objects. The first method I call when a new match is created, and the second method is called when the app is first opened, right after the current matches are loaded. They insure that the loadPlayers method is only called once at app loading, and once per new match. There are a few problems with this setup although.

For one, the latter two methods have no way of communicating with the former method regarding making async web requests. In other words, getPlayersForMatches could be called, immediately followed by playerForPlayerID. This would start two async web requests for the same information, with potentially bad results. There's also the case where the async web request fails to get a certain GKPlayer for a playerID, but still calls the delegate's playersLoaded method. Thus, an infinite loop will be formed.

If I ever get around to optimizing these methods, I'll post the new ones below. Also, make sure to comment if you have any questions. Also, sorry about the code formatting, haven't figured out how to make it format properly yet...

No comments:

Post a Comment