Handling Game Center authentication
Asked Answered
K

4

6

According to the Apple docs we should do something like this to handle GC authentication:

- (void) authenticateLocalUser
{
    GKLocalPlayer *localPlayer = [GKLocalPlayer localPlayer];

    if(localPlayer.authenticated == NO)
    {
        [localPlayer setAuthenticateHandler:(^(UIViewController* viewcontroller, NSError *error) {
            if (!error && viewcontroller)
            {
                DLog(@"Need to log in");
                AppDelegate *appDelegate = (AppDelegate*)[[UIApplication sharedApplication] delegate];
                [appDelegate.window.rootViewController presentViewController:viewcontroller animated:YES completion:nil];

            }
            else
            {
                DLog(@"Success");

            }
        })];

    }
}

And we are given this information:

If the device does not have an authenticated player, Game Kit passes a view controller to your authenticate handler. When presented, this view controller displays the authentication user interface. Your game should pause other activities that require user interaction (such as your game loop), present this view controller and then return. When the player finishes interacting with it, the view controller is dismissed automatically.

My question is, how do we know when this view controller gets dismissed, and how do we know if the authentication succeeded or not?

Obviously I need to know if the authentication worked or not, and I need to know when to resume the game if I had to pause it because the magic GC view controller was presented.

Kozloski answered 29/11, 2013 at 18:59 Comment(1)
Just wondering in 2020 - is this thread still relevant, or is there some new functionality? I've found myself having to implement a similar sort of fairly in-elegant solution to get around this. Seems like the help around the latest iterations of GameKit is pretty sparse.Hexahedron
F
14

There is a problem with your code: First and foremost, you should set the authentication handler as soon as your app loads. This means that regardless of whether the localPlayer is authenticated or not, you set the handler so that it is automatically called if the player is logged out and logged back in again. If your player switches from your app to the game center app, and logs out / in, then the handler in your app won't be called (if he was already authenticated when the app first started up). The point of setting the handler is so that every time there is an auth change (in / out), your app can do the right thing.

Secondly, you shouldn't be relying on the error for anything. Even if an error is returned, game kit may still have enough cached information to provide an authenticated player to your game. The errors are only to assist you with debugging.

To answer your questions, first review my code example below.

-(void)authenticateLocalPlayer
{
    GKLocalPlayer *localPlayer = [GKLocalPlayer localPlayer];

    //Block is called each time GameKit automatically authenticates
    localPlayer.authenticateHandler = ^(UIViewController *viewController, NSError *error)
    {
        [self setLastError:error];
        if (viewController)
        {
            self.authenticationViewController = viewController;
            [self disableGameCenter];
        }
        else if (localPlayer.isAuthenticated)
        {
            [self authenticatedPlayer];
        }
        else
        {
            [self disableGameCenter];
        }
    };
}

-(void)authenticatedPlayer
{
     GKLocalPlayer *localPlayer = [GKLocalPlayer localPlayer];
    [[NSNotificationCenter defaultCenter]postNotificationName:AUTHENTICATED_NOTIFICATION object:nil];
    NSLog(@"Local player:%@ authenticated into game center",localPlayer.playerID);
}

-(void)disableGameCenter
{
    //A notification so that every observer responds appropriately to disable game center features
    [[NSNotificationCenter defaultCenter]postNotificationName:UNAUTHENTICATED_NOTIFICATION object:nil];
    NSLog(@"Disabled game center");
}

In my app, the call to authenticateLocalPlayer is made only once, when the app is launched. This is because the handler is invoked automatically after that.

how do we know when this view controller gets dismissed,

You won't know when this view controller gets dismissed. The code example in the documentation says to show the view controller at the appropriate time. This means that you shouldn't necessarily show the view controller every time that game center isn't able to log in. In fact, you probably shouldn't present it immediately in the handler. You should show the view controller only when it is necessary for your player to proceed with the task at hand. It shouldn't pop up at a weird time. That is why I save the view controller, so I can display later when it makes sense to.

I need to know when to resume the game if I had to pause it because the magic GC view controller was presented.

If you setup your authentication handler to post notifications based on status changes, you can listen for the event and show a "pause menu" or something, that remains until the user chooses to resume.

how do we know if the authentication succeeded

If the authentication succeeded, then the view controller is nil, and localPlayer.isAuthenticated is true.

or not ?

If authentication failed, then localPlayer.isAuthenticated is false, and the view controller was nil. Authentication failing could have happened for a number of reasons (network etc), and you shouldn't be presenting the view controller in this case, which is why the view controller will be nil.In this scenario, you should disable game center features until the user is next logged in. Since the authentication handler is called automatically, most of the time you shouldn't need to do anything. You can always provide a means to launch the game center app from your app, if you want to prompt the user to do something in game center, which you can't do automatically through your code.

EDIT: using a flag like self.isAuthenticated (as I did above)to keep track of whether you are logged in or not is not a great idea (I didn't want to cause any confusion, so I didn't remove it). It is better to always check [GKLocalPlayer localPlayer].isAuthenticated

EDIT: Cleaned up code a bit - removed unnecessary self.isAuthenticated, and block variable which isn't required.

Flagler answered 29/11, 2013 at 18:59 Comment(14)
hi, can you please explain the need for the blockLocalPlayer variable here? This isn't clear to me. Why not use the localPlayer var directly?Noticeable
It isn't necessary - As I answered this question a year ago, I can only imagine that in my project from which I cut and paste most of the code I was doing something with blocks. I've edited the answer. If you look at the apple example from which my code is based you won't need the block variable : developer.apple.com/library/ios/documentation/…Hendecagon
ya, I'm still not super comfy with blocks. Just checking to make sure I didn't miss something. thanks.Noticeable
How do you "setup your authentication handler to post notifications based on status changes"?Grating
Actually, you will need the blockLocalPlayer and you'll need to designate it __weak. Otherwise, the compiler will warn you about retain cycles, bc localPlayer has a strong reference to itself via the block.Balky
@JamesPMason I haven't used Game Center in a long time, and don't remember anything. Looking at my code posted above, it looks like I have done precisely that - Notifications are triggered when authenticatedPlayer or disableGameCenter are called. You can read up on how to listen for notifications. There are no shortage of answers about that on stackoverflow and elsewhere.Hendecagon
@PhilMitchell I don't remember. Its been a long time since I used this code, and don't have time to check. I made the answer a community wiki, so if there is anything wrong you can edit it. Or if you feel it is very misleading I can delete it. Also, you should file a bug on apples documentation, as it means their example has a bug in it! XDHendecagon
@Flagler I don't think disableGameCenter is the same as a notification of the authentication view controller being dismissed. At least, that's not what the verbiage implies. I'm successfully using notifications for all kinds of things related to game center already, it's only this one important case I'm confused on.Grating
disableGameCenter isn't used to send notifications saying that the view controller is dismissed. Its used to tell listeners that game center isn't available.Hendecagon
@Flagler That's my point. In your first comment you said "Notifications are triggered when authenticatedPlayer or disableGameCenter are called" but that doesn't address the question of what the notification is for a dismissed authentication view controller, or where to set it up.Grating
I can't help you because I worked on this so long ago, and don't remember how I dealt with this problem (if i did). As far as I remember, Apple's documentation suggests that you display the view controller only at times when it is relevant to the player, and when it can be dealt with in an appropriate manner. I set the authenticate handler at startup, so it is triggered whenever there is a change in the users status. You can display the view controller whenever you like (perhaps outside a game).Hendecagon
A user normally is logged out of game center when he explicitly chooses to log out. Otherwise there is cached information so even if there isn't network access they aren't normally logged out. So most of the time you can don't need to know when the view controller is dismissed, and the notifications are enough to enable / disable features like multiplayer. I don't have time to search for the last project I worked on, but I believe that your problem shouldn't be difficult to solve, and you don't need to know when the view controller is dismissed.Hendecagon
You should focus on implementing your flow, so it doesn't matter when the controller is dismissed, and your UI responds to the notifications gracefully. Also, you probably shouldn't interrupt a game in the middle by showing this view controller.Hendecagon
I'm considering taking your advice and just requiring game center sign in before gameplay can begin. I didn't want to do that because of the recommendation made at WWDC a couple years ago not to slow down entrance to game play in any way, and even to proceed without game center at first if possible. I already store all game progression stats to disk so I don't need access to game center to begin the game.Grating
S
5

For some reason, the Game Center authentication view controller is an instance of GKHostedAuthenticateViewController which is a private class we're not allowed to use or reference. It doesn't give us any way to cleanly detect when it is dismissed (unlike instances of GKGameCenterViewController which allow us to via the GKGameCenterControllerDelegate protocol.

This solution (read workaround) works by testing in the background every quarter of a second for when the view controller has been dismissed. It's not pretty, but it works.

The code below should be part of your presentingViewController, which should conform to the GKGameCenterControllerDelegate protocol.

Swift and Objective-C provided.

// Swift
func authenticateLocalUser() {
    if GKLocalPlayer.localPlayer().authenticateHandler == nil {
        GKLocalPlayer.localPlayer().authenticateHandler = { (gameCenterViewController: UIViewController?, gameCenterError: NSError?) in
            if let gameCenterError = gameCenterError {
                log.error("Game Center Error: \(gameCenterError.localizedDescription)")
            }

            if let gameCenterViewControllerToPresent = gameCenterViewController {
                self.presentGameCenterController(gameCenterViewControllerToPresent)
            }
            else if GKLocalPlayer.localPlayer().authenticated {
                // Enable GameKit features
                log.debug("Player already authenticated")
            }
            else {
                // Disable GameKit features
                log.debug("Player not authenticated")
            }
        }
    }
    else {
        log.debug("Authentication Handler already set")
    }
}

func testForGameCenterDismissal() {
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(0.25 * Double(NSEC_PER_SEC))), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)) {
        if let presentedViewController = self.presentedViewController {
            log.debug("Still presenting game center login")
            self.testForGameCenterDismissal()
        }
        else {
            log.debug("Done presenting, clean up")
            self.gameCenterViewControllerCleanUp()
        }
    }
}

func presentGameCenterController(viewController: UIViewController) {
    var testForGameCenterDismissalInBackground = true

    if let gameCenterViewController = viewController as? GKGameCenterViewController {
        gameCenterViewController.gameCenterDelegate = self
        testForGameCenterDismissalInBackground = false
    }

    presentViewController(viewController, animated: true) { () -> Void in
        if testForGameCenterDismissalInBackground {
            self.testForGameCenterDismissal()
        }
    }
}

func gameCenterViewControllerDidFinish(gameCenterViewController: GKGameCenterViewController!) {
    gameCenterViewControllerCleanUp()
}

func gameCenterViewControllerCleanUp() {
    // Do whatever needs to be done here, resume game etc
}

Note: the log.error and log.debug calls are referencing XCGLogger: https://github.com/DaveWoodCom/XCGLogger

// Objective-C
- (void)authenticateLocalUser
{
    GKLocalPlayer* localPlayer = [GKLocalPlayer localPlayer];

    __weak __typeof__(self) weakSelf = self;
    if (!localPlayer.authenticateHandler) {
        [localPlayer setAuthenticateHandler:(^(UIViewController* viewcontroller, NSError* error) {
            if (error) {
                DLog(@"Game Center Error: %@", [error localizedDescription]);
            }

            if (viewcontroller) {
                [weakSelf presentGameCenterController:viewcontroller];
            }
            else if ([[GKLocalPlayer localPlayer] isAuthenticated]) {
                // Enable GameKit features
                DLog(@"Player already authenticated");
            }
            else {
                // Disable GameKit features
                DLog(@"Player not authenticated");
            }
        })];
    }
    else {
        DLog(@"Authentication Handler already set");
    }
}

- (void)testForGameCenterDismissal
{
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
        if (self.presentedViewController) {
            DLog(@"Still presenting game center login");
            [self testForGameCenterDismissal];
        }
        else {
            DLog(@"Done presenting, clean up");
            [self gameCenterViewControllerCleanUp];
        }
    });
}

- (void)presentGameCenterController:(UIViewController*)viewController
{
    BOOL testForGameCenterDismissalInBackground = YES;
    if ([viewController isKindOfClass:[GKGameCenterViewController class]]) {
        [(GKGameCenterViewController*)viewController setGameCenterDelegate:self];
        testForGameCenterDismissalInBackground = NO;
    }

    [self presentViewController:viewController animated:YES completion:^{
        if (testForGameCenterDismissalInBackground) {
            [self testForGameCenterDismissal];
        }
    }];
}

- (void)gameCenterViewControllerDidFinish:(GKGameCenterViewController*)gameCenterViewController
{
    [self gameCenterViewControllerCleanUp];
}

- (void)gameCenterViewControllerCleanUp
{
    // Do whatever needs to be done here, resume game etc
}
Separatist answered 16/12, 2014 at 5:18 Comment(3)
Hi Dave, I have a quick question that I'm hoping you can answer without me having to provide code (otherwise I'll post a question and give you the link). My question has to do mainly with showing the game center view controller. When I do "presentGameController" my game screen presents a UIViewController, but its just a black screen. any idea why this may be happening?Asexual
Sorry, that's not something I've seen happen, so I'd only be guessing. I'd post a question with some sample code.Separatist
thanks for the quick response! I'll post a question and let you knowAsexual
B
1

I might be wrong, but I think there actually is a way to know when the authentication view controller gets dismissed. I believe the initial authenticate handler that you set will be called when the user dismisses the authentication view controller, except this time the viewController parameter of the handler will be nil.

The way my app works is: the authenticate handler is set at the beginning of the application, but the authentication view controller is only displayed when the user asks to view the Leaderboards. Then, when this authentication view controller is dismissed, the initial authenticate handler either displays the leaderboards if the user was authenticated or doesn't if he wasn't.

Butanone answered 9/4, 2014 at 23:20 Comment(0)
S
-1

The Game Center DELEGATE method: 'gameCenterViewControllerDidFinish' is called automatically when the Game Center viewController is 'Done'. (This is a compulsory method for the delegate.)

You can put whatever you need for your app, in this method.

Stibine answered 6/10, 2014 at 15:16 Comment(2)
Most of the tutorials I've found don't use this delegate at all. Do you have an example of how to set up game center with the delegate included?Grating
This delegate doesn't work for the login view controller which is an instance of GKHostedAuthenticateViewController (private), verses an instance of GKGameCenterViewController used by the GKGameCenterControllerDelegate protocol.Separatist

© 2022 - 2024 — McMap. All rights reserved.