Building a SpriteKit/GameKit leaderboard within a specific scene

这一生的挚爱 提交于 2019-12-18 11:33:55

问题


I'm pretty new to Swift and I'm having some trouble implementing a leaderboard into my game. I just watched a tutorial: 'Game Center Leaderboards! (Swift 2 in Xcode)' in which the GameCenter information all went through the one view of the app. In my game, I want the user to be able to play the game and then only when they are on a specific SKScene will they have access to GameCenter.

So for example, on the GameOverScene will they be user authenticated and also will be able to upload their high score. I think I'm also missing some of the differences between the GameViewController (where all of the tutorials logic is located) and one of my many scenes that I've made.

Here is my code in which I attempt to use the GKGameCenterControllerDelegate on the GameOverScene and create the various functions to reach GameCenter. The call is made when the user taps a certain label in the view: (this clearly doesnt work as I'm trying to access a scene on lines like this: self.presentViewController(view!, animated:true, completion: nil)


class GameOverScene: SKScene, GKGameCenterControllerDelegate  {

    init(size: CGSize, theScore:Int) {
        score = theScore
        super.init(size: size)
    }
    ...

    override func didMoveToView(view: SKView) {

        authPlayer()

        leaderboardLabel.text = "Tap for Leaderboard"
        leaderboardLabel.fontSize = 12
        leaderboardLabel.fontColor = SKColor.redColor()
        leaderboardLabel.position = CGPoint(x: size.width*0.85, y: size.height*0.1)
        addChild(leaderboardLabel)

        ...

    override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {

        for touch : AnyObject in touches {
            let location = touch.locationInNode(self)

            if(CGRectContainsPoint(leaderBoardLabel.frame, location)){
                saveHighScore(score)
                showLeaderBoard()
            }
        }
    }


    func authPlayer(){

        //Create a play
        let localPlayer = GKLocalPlayer.localPlayer()

        //See if signed in or not
        localPlayer.authenticateHandler = {
            //A view controller and an error handler
            (view,error) in

            //If there is a view to work with
            if view != nil {
                self.presentViewController(view!, animated:true, completion: nil) //we dont want a completion handler
            }

            else{
                print(GKLocalPlayer.localPlayer().authenticated)
            }
        }
    }


    //Call this when ur highscore should be saved
    func saveHighScore(number:Int){

        if(GKLocalPlayer.localPlayer().authenticated){

            let scoreReporter = GKScore(leaderboardIdentifier: "scoreBoard")
            scoreReporter.value = Int64(number)

            let scoreArray: [GKScore] = [scoreReporter]

            GKScore.reportScores(scoreArray, withCompletionHandler: nil)

        }

    }


    func showLeaderBoard(){

        let viewController = self.view.window?.rootViewController
        let gcvc = GKGameCenterViewController()

        gcvc.gameCenterDelegate = self

        viewController?.presentViewController(gcvc, animated: true, completion: nil)


    }


    func gameCenterViewControllerDidFinish(gameCenterViewController: GKGameCenterViewController) {
        gameCenterViewController.dismissViewControllerAnimated(true, completion: nil)
    }

Any advice on how I could go about this would be great, I think that I may be getting the whole scene/view controller mixed up and its leading to problems. Thanks!


回答1:


This answer partially carries off on where we left off in the comments, since you didn't post your entire code I can't tell exactly where your hangup was, but this is what I put together along with a separate guide (it's a slightly different version of the code you post):

Author of most of the code:

https://www.reddit.com/r/swift/comments/3q5owv/how_to_add_a_leaderboard_in_spritekit_and_swift_20/

GameViewController.swift:

import UIKit
import SpriteKit
import GameKit

class GameViewController: UIViewController {

    func authenticateLocalPlayer() {
        let localPlayer = GKLocalPlayer.localPlayer()
        localPlayer.authenticateHandler = {(viewController, error) -> Void in

            if (viewController != nil) {
                self.presentViewController(viewController!, animated: true, completion: nil)
            }
            else {
                print((GKLocalPlayer.localPlayer().authenticated))
            }
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        /////authentication//////
        authenticateLocalPlayer()

        //... The rest of the default code
    }

    //... The rest of the default code
}

GameScene.swift (or whichever scene you want to use GC):


import SpriteKit
import GameKit
import UIKit

// Global scope (I generally put these in a new file called Global.swift)
var score = 0


//sends the highest score to leaderboard
func saveHighscore(gameScore: Int) {
    print ("You have a high score!")
    print("\n Attempting to authenticating with GC...")

    if GKLocalPlayer.localPlayer().authenticated {
        print("\n Success! Sending highscore of \(score) to leaderboard")

        //---------PUT YOUR ID HERE:
        //                          |
        //                          |
        //                          V
        let my_leaderboard_id = "YOUR_LEADERBOARD_ID"
        let scoreReporter = GKScore(leaderboardIdentifier: my_leaderboard_id)

        scoreReporter.value = Int64(gameScore)
        let scoreArray: [GKScore] = [scoreReporter]

        GKScore.reportScores(scoreArray, withCompletionHandler: {error -> Void in
            if error != nil {
                print("An error has occured:")
                print("\n \(error) \n")
            }
        })
    }
}

// Your scene:
class GameScene: SKScene, GKGameCenterControllerDelegate {

    // Local scope variables (for this scene):

    // Declare a new node, then initialize it
    let call_gc_node   = SKLabelNode(fontNamed:"Chalkduster")
    let add_score_node = SKLabelNode(fontNamed: "Helvetica")


    override func didMoveToView(view: SKView) {

        // Give our GameCenter node some stuff
        initGCNode: do {

            // Set the name of the node (we will reference this later)
            call_gc_node.name = "callGC"

            // Default inits
            call_gc_node.text = "Send your HighScore of \(score) into Game Center"
            call_gc_node.fontSize = 25
            call_gc_node.position = CGPoint(
                x:CGRectGetMidX(self.frame),
                y:CGRectGetMidY(self.frame))

            // Self here is the instance (object) of our class, GameScene
            // This adds it to our view
            self.addChild(call_gc_node)
        }

        // Give our Add label some stuff
        initADDLabel: do {

            // Set the name of the node (we will reference this later)
            add_score_node.name = "addGC"

            // Basic inits
            add_score_node.text = "ADD TO SCORE!"
            add_score_node.fontSize = 25
            add_score_node.position = call_gc_node.position

            // Align our label some
            add_score_node.runAction(SKAction.moveByX(0, y: 50, duration: 0.01))

            // Add it to the view
            self.addChild(add_score_node)
        }

    }


    override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
        for touch in touches {

            // Get the position of our click
            let TPOINT = touch.locationInNode(self)

            // Get the name (string) of the node that was touched
            let
                node_that_was_touched: String?
                                                = nodeAtPoint(TPOINT).name


            // Prepare for switch statement, when we unwrap the optional, we don't want nil
            guard (node_that_was_touched != nil)
                else { print("-> before switch: found nil--not entering Switch");
                    return
            }


            // Find out which node we clicked based on node.name?, then do stuff:
            switch node_that_was_touched! {

                case "callGC":
                    // We clicked the GC label:

                    GameOver: do {

                        print("GAME OVER!")

                        // If we have a high-score, send it to leaderboard:
                        overrideHighestScore(score)

                        // Reset our score (for the next playthrough)
                        score = 0

                        // Show us our stuff!
                        showLeader()
                    }

                case "addGC":
                    // we clicked the Add label:

                    // Update our *current score*
                    score += 1


                default: print("no matches found")
            }

        }

    }


    override func update(currentTime: CFTimeInterval) {
        /* Called before each frame is rendered */
        call_gc_node.text = "Send your HighScore of \(score) into Game Center"

    }


    // Gamecenter
    func gameCenterViewControllerDidFinish(gameCenterViewController: GKGameCenterViewController) {
        gameCenterViewController.dismissViewControllerAnimated(true, completion: nil)
    }

    //shows leaderboard screen
    func showLeader() {
        let viewControllerVar = self.view?.window?.rootViewController
        let gKGCViewController = GKGameCenterViewController()
        gKGCViewController.gameCenterDelegate = self
        viewControllerVar?.presentViewController(gKGCViewController, animated: true, completion: nil)
    }

    // Your "game over" function call
    func overrideHighestScore(gameScore: Int) {
        NSUserDefaults.standardUserDefaults().integerForKey("highscore")
        if gameScore > NSUserDefaults.standardUserDefaults().integerForKey("highscore")
        {
            NSUserDefaults.standardUserDefaults().setInteger(gameScore, forKey: "highscore")
            NSUserDefaults.standardUserDefaults().synchronize()

            saveHighscore(gameScore)
        }
    }
}

Pay special attention to

29: let my_leaderboard_id = "YOUR_LEADERBOARD_ID"

I put some silly ASCII art in there to make sure you don't miss it. You have to put in your actual leaderboard ID from the GameCenter set-up.

You have to add the GameCenter library as well, and do the iTunes connect to actually see your highscores in the pop-up window.

I think your initial problems were with not understanding some of the back-end of how SpriteKit and even iOS views work (which is totally fine, because Apple makes jumping in and making stuff very easy). But, as you see, following guides / tutorials can be difficult since your implementation will vary.

Here is some good info to start with:

Diagram of what happens each frame in SK:


So you see, the SKScene is the class with all of the fun stuff like Nodes and Actions, and is where everything (important to you) happens. You can generate these scenes through the Editor, but then you probably need to make a new .swift file to go with it (as each scene can have its own logic).

The editor is just a 'shortcut' to initializing a bunch of stuff, and honestly, you can make complete games with little code (but you very quickly find out that you want more)

So in this code, where you declare GameScene or PauseScreen (which are basically just class declarations, that inherit from SKScene), you quickly find this line talking about something that ISNT a scene:

override func didMoveToView(view: SKView) .. it's calling a SKView... what is that, and where did it come from?

(Read about SKView here, and look at its inheritance):

https://developer.apple.com/library/ios/documentation/SpriteKit/Reference/SKView/index.html#//apple_ref/occ/cl/SKView


We find this SKView declaration in the GameViewController file, (which is just a class), notice that it's the same as the regular iOS apps mostly, as it inherits UIViewController:

override func viewDidLoad() {
    super.viewDidLoad()
    if let scene = GameScene(fileNamed:"GameScene") {
        // Configure the view.
        let skView = self.view as! SKView
        skView.showsFPS = true
        skView.showsNodeCount = true

        /* Sprite Kit applies additional optimizations to improve               rendering performance */
        skView.ignoresSiblingOrder = true

        /* Set the scale mode to scale to fit the window */
        scene.scaleMode = .AspectFill

        skView.presentScene(scene)
    }

Again, that method is declared in GameViewController.swift, which is basically just this: class GameViewController: UIViewController


So how does all of this relate to iOS apps and SpriteKit? Well, they are all mashed on top of each other:

IOS app anatomy:

Basically, from right to left, you have the Window, which is (correct me if wrong) the AppDelegate, then the ViewController, then your View, which has all of the cool stuff in it (Storyboards sit inside of the View, just as SKScenes sit inside of the View.... Labels, Nodes, or Buttons, all sit inside of their respective classes ((the view)))

It's all a big sandwich of inheritance.


Check out the Apple websites for more info.

https://developer.apple.com/library/safari/documentation/UserExperience/Conceptual/MobileHIG/ContentViews.html#//apple_ref/doc/uid/TP40006556-CH13-SW1

https://developer.apple.com/spritekit/

https://developer.apple.com/library/ios/documentation/SpriteKit/Reference/SpriteKitFramework_Ref/

https://developer.apple.com/library/safari/documentation/UserExperience/Conceptual/MobileHIG/Anatomy.html

Basically, everything is an Class inherited from a class inherited from a class and so on, so on... It can get messy. You can also see these inheritances in Xcode by CMD+clicking on them, which will jump you to the source file.

Goodluck with your studies and adventures in SpriteKit :)




回答2:


Detailed Answer that still works in my game as of Swift 2.2 and partially Xcode 7.1 I wrote a while ago. Detailed answer, but just skip to the bottom. To basically answer your question, showLeaderboard() would be called whenever you want it to be called, just put the specific function in the right SKScene class.

iTunes Connect:

1) Log in into your iTunes Connect account. Go to My Apps, and select the app you want leaderboards with.

2) Go to Features, and then Game Center. Click the plus sign to create a leaderboard. If you want to make a set of leaderboards (grouped leaderboards, then go to the right and click on "More".

3) After clicking the plus sign, follow the instructions of what kind of leaderboard you want. At first, do a single leaderboard if you're not sure. The "Leaderboard ID" you assign to it will be used in your code as a string when accessing it, so make sure you type something nice.

Now in xCode:

1) Include the GameKit.framework library by choosing the "+" sign.

2) Add the string "GameKit" into your info.plist

3a) Add the following on top of the GameViewController.swift file with the other import code.

import GameKit

3b) Add the following function inside the class in the same swift file.

    func authenticateLocalPlayer() {
    let localPlayer = GKLocalPlayer.localPlayer()
    localPlayer.authenticateHandler = {(viewController, error) -> Void in

        if (viewController != nil) {
            self.presentViewController(viewController!, animated: true, completion: nil)
        }
        else {
            print((GKLocalPlayer.localPlayer().authenticated))
        }
    }
}

4) Call the "authenticateLocalPlayer" function from inside the viewDidLoad() function.

5a) Now, go to the GameScene.swift file (or wherever the execution will take place). And also add the following on the top.

import GameKit

5b) Add the following code inside the class function.

//shows leaderboard screen
func showLeader() {
    let viewControllerVar = self.view?.window?.rootViewController
    let gKGCViewController = GKGameCenterViewController()
    gKGCViewController.gameCenterDelegate = self
    viewControllerVar?.presentViewController(gKGCViewController, animated: true, completion: nil)
}
func gameCenterViewControllerDidFinish(gameCenterViewController: GKGameCenterViewController) {
    gameCenterViewController.dismissViewControllerAnimated(true, completion: nil)
}

In my game, I have a button to display the leaderboards, so wherever that may be, just call the "showLeader" function to display the leaderboards.

6) You must have a function that sends the score to the leaderboards. Outside of the entire class declaration, I have the following function that accepts the user's game score as a parameter and sends it to the leaderboard. Where it says "YOUR_LEADERBOARD_ID", that is where your Leaderboard ID I mentioned earlier goes in. As pictured here.

//sends the highest score to leaderboard
func saveHighscore(gameScore: Int) {

    print("Player has been authenticated.")

    if GKLocalPlayer.localPlayer().authenticated {

        let scoreReporter = GKScore(leaderboardIdentifier: "YOUR_LEADERBOARD_ID")
        scoreReporter.value = Int64(gameScore)
        let scoreArray: [GKScore] = [scoreReporter]

        GKScore.reportScores(scoreArray, withCompletionHandler: {error -> Void in
            if error != nil {
                print("An error has occured: \(error)")
            }
        })
    }
}

7) This is the function I have that is called on a game over. It decides if the score is greater than the previous highest score, and if it is, it will send it to the leaderboards. This code is entirely up to you, but make sure it calls the saveHighscore function which sends the data to the leaderboard.

func overrideHighestScore(gameScore: Int) {
    NSUserDefaults.standardUserDefaults().integerForKey("highscore")
    if gameScore > NSUserDefaults.standardUserDefaults().integerForKey("highscore") {

        NSUserDefaults.standardUserDefaults().setInteger(gameScore, forKey: "highscore")
        NSUserDefaults.standardUserDefaults().synchronize()

        saveHighscore(gameScore)
    }
}

Notice the function "saveHighscore" is called in the end.

8) Finally, in the game over function (or whatever you called it), call the function "overrideHighestScore" so it can check if the score is good enough to save.

After doing both, wait a few minutes until the leaderboards is connected to your app (or that they do load). It works on my game. Hopefully, I haven't forgotten any step. Screenshot of my game leaderboards that were used to make this tutorial.



来源:https://stackoverflow.com/questions/39149404/building-a-spritekit-gamekit-leaderboard-within-a-specific-scene

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!