Bear with me here, as there is a lot to explain!
I am working on implementing a Game Center high score leaderboard into my game. I have looked around for examples of how to properly implement this code, but have come up short on finding much material. Therefore, I have tried implementing it myself based off information I found on apples documentation.
Long story short, I am getting success printed when I update my score, but no scores are actually being posted (or at-least no scores are showing up on the Game Center leaderboard when opened).
Before I show the code, one thing I have questioned is the fact that this game is still in development. In AppStoreConnect, the status of the leaderboard is "Not Live". Does this affect scores being posted?
Onto the code. I have created a GameCenter class which handles getting the leaderboards and posting scores to a specific leaderboard. I will post the code in whole, and will discuss below what is happening.
class Leaderboard
{
var id : String
var leaderboard : GKLeaderboard
var loaded : Bool
init()
{
self.id = ""
self.leaderboard = GKLeaderboard()
self.loaded = false
}
init(id: String, leaderboard: GKLeaderboard, loaded: Bool)
{
self.id = id
self.leaderboard = leaderboard
self.loaded = loaded
}
}
class GameCenter
{
static let shared = GameCenter()
private var player = GKLocalPlayer.local
private var leaderboards : [Leaderboard] = []
func authenticatePlayer()
{
player.authenticateHandler = { (vc, error) -> Void in
if let error = error
{
print(error.localizedDescription)
}
else if let vc = vc
{
if let viewController = UIApplication.shared.windows.first!.rootViewController
{
viewController.present(vc, animated: true)
{
self.loadLeaderboards()
}
}
}
else
{
self.loadLeaderboards()
}
}
}
func loadLeaderboards()
{
var leaderboardIDs : [String] = []
// Gather all of the leaderboard ids that we have
for leaderboard in GameCenterLeaderboards.allCases
{
leaderboardIDs.append(leaderboard.rawValue)
}
// Load the leaderboard for all of these ids and add to a new array
GKLeaderboard.loadLeaderboards(IDs: leaderboardIDs) { (loadedLeaderboards, error) in
if let error = error
{
print(error.localizedDescription)
return
}
if let loadedLeaderboards = loadedLeaderboards
{
print("\n--- Loaded Leaderboards ---")
for loadedBoard in loadedLeaderboards
{
let board = Leaderboard(id: loadedBoard.baseLeaderboardID, leaderboard: loadedBoard, loaded: true)
self.leaderboards.append(board)
print("ID: \(board.id)")
}
print("\n")
self.updateLocalHighScore()
}
}
}
func playerAuthenticated() -> Bool
{
return player.isAuthenticated
}
func submitScore(id: String)
{
if ( playerAuthenticated() )
{
let leaderboard = getLeaderboard(id: id)
if ( leaderboard.loaded )
{
print("Submitting score of \(AppSettings.shared.highScore!) for leaderboard \(leaderboard.id)")
leaderboard.leaderboard.submitScore(AppSettings.shared.highScore, context: -1, player: player) { (error) in
if let error = error
{
print(error.localizedDescription)
}
else
{
print("Successfully submitted score to leaderboard")
}
}
}
}
}
func getLeaderboard(id: String) -> Leaderboard
{
if let leaderboard = leaderboards.first(where: { $0.id == id } )
{
return leaderboard
}
return Leaderboard()
}
func updateLocalHighScore()
{
let leaderboard = getLeaderboard(id: GameCenterLeaderboards.HighScore.rawValue)
if ( leaderboard.loaded )
{
leaderboard.leaderboard.loadEntries(for: [player], timeScope: .allTime) { (playerEntry, otherEntries, error) in
if let error = error
{
print(error.localizedDescription)
return
}
if let score = playerEntry?.score
{
print("Player Score in leaderboard: \(score)")
if( score > AppSettings.shared.highScore )
{
AppSettings.shared.highScore = score
print("High Score Updated!")
}
else
{
// Lets post the local high score to game center
self.submitScore(id: leaderboard.id)
print("Local High Score Is Greating, requesting a submit!")
}
}
}
}
}
}
In a different GameScene, once the game is over, I request to post a new high score to Game Center with this line of code:
GameCenter.shared.submitScore(id: GameCenterLeaderboards.HighScore.rawValue)
The last thing I have questions about is the context when submitting a score. According to the documentation, this seems to just be metadata that GameCenter does not care about, but rather something the developer can use. Therefore, I think I can cross this off as causing the problem.
I believe I implemented this correctly, but for some reason, nothing is posting to the leaderboard. This was ALOT, but I wanted to make sure I got all my thoughts down.
Any help on why this is NOT posting would be awesome! Thanks so much!
Mark
I had the same problem - submitting scores to Game Center by using a new GameKit API available from iOS 14 was never actually saved on the Game Center side (even though there was no any error reported).
Solution which worked for me in the end was simply using a Type Method (with my leaderboardID):
class func submitScore(_ score: Int,
context: Int,
player: GKPlayer,
leaderboardIDs: [String],
completionHandler: #escaping (Error?) -> Void)
instead of the Instance Method counterpart (which apparently has some bug on Apple side):
func submitScore(_ score: Int,
context: Int,
player: GKPlayer,
completionHandler: #escaping (Error?) -> Void)
I was going crazy over this, so I hope this helps someone else.
Related
I'm a student studying iOS development currently working on a simple AI project that utilizes SNAudioStreamAnalyzer to classify an incoming audio stream from the device's microphone. I can start the stream and analyze audio no problem, but I've noticed I can't seem to get my app to stop analyzing and close the audio input stream when I'm done. At the beginning, I initialize the audio engine and create the classification request like so:
private func startAudioEngine() {
do {
// start the stream of audio data
try audioEngine.start()
let snoreClassifier = try? SnoringClassifier2_0().model
let classifySoundRequest = try audioAnalyzer.makeRequest(snoreClassifier)
try streamAnalyzer.add(classifySoundRequest,
withObserver: self.audioAnalyzer)
} catch {
print("Unable to start AVAudioEngine: \(error.localizedDescription)")
}
}
After I'm done classifying my audio stream, I attempt to stop the audio engine and close the stream like so:
private func terminateNight() {
streamAnalyzer.removeAllRequests()
audioEngine.stop()
stopAndSaveNight()
do {
let session = AVAudioSession.sharedInstance()
try session.setActive(false)
} catch {
print("unable to terminate audio session")
}
nightSummary = true
}
However, after I call the terminateNight() function my app will continue using the microphone and classifying the incoming audio. Here's my SNResultsObserving implementation:
class AudioAnalyzer: NSObject, SNResultsObserving {
var prediction: String?
var confidence: Double?
let snoringEventManager: SnoringEventManager
internal init(prediction: String? = nil, confidence: Double? = nil, snoringEventManager: SnoringEventManager) {
self.prediction = prediction
self.confidence = confidence
self.snoringEventManager = snoringEventManager
}
func makeRequest(_ customModel: MLModel? = nil) throws -> SNClassifySoundRequest {
if let model = customModel {
let customRequest = try SNClassifySoundRequest(mlModel: model)
return customRequest
} else {
throw AudioAnalysisErrors.ModelInterpretationError
}
}
func request(_ request: SNRequest, didProduce: SNResult) {
guard let classificationResult = didProduce as? SNClassificationResult else { return }
let topClassification = classificationResult.classifications.first
let timeRange = classificationResult.timeRange
self.prediction = topClassification?.identifier
self.confidence = topClassification?.confidence
if self.prediction! == "snoring" {
self.snoringEventManager.snoringDetected()
} else {
self.snoringEventManager.nonSnoringDetected()
}
}
func request(_ request: SNRequest, didFailWithError: Error) {
print("ended with error \(didFailWithError)")
}
func requestDidComplete(_ request: SNRequest) {
print("request finished")
}
}
It was my understanding that upon calling streamAnalyzer.removeAllRequests() and audioEngine.stop() the app would stop streaming from the microphone and call the requestDidComplete function, but this isn't the behavior I'm getting. Any help is appreciated!
From OP's edition:
So I've realized it was a SwiftUI problem. I was calling the startAudioEngine() function in the initializer of the view it was declared on. I thought this would be fine, but since this view was embedded in a parent view when SwiftUI updated the parent it was re-initializing my view and as such calling startAudioEngine() again. The solution was to call this function in on onAppear block so that it activates the audio engine only when the view appears, and not when SwiftUI initializes it.
I don't believe you should expect to receive requestDidComplete due to removing a request. You'd expect to receive that when you call completeAnalysis.
I am trying to make a document based app in swiftUI with a custom UI. I want iCloud capabilities in my app. I am trying to use iCloud Document (No cloudKit) way for storing data on iCloud container. I am using UIDocument and it's working. It's storing data to iCloud and I am able to retrieve it back.
Now the thing is when I run the app on two devices (iphone and iPad) and make changes to a file on one device, the changes are not reflecting on the other device while the file or say app is open. I have to close the app and relaunch it to see the changes.
I know I have to implement NSMetadataQuery to achieve this but I am struggling with it. I don't know any objective-C. I have been searching on the internet for a good article but could not find any. Can you please tell how do I implement this feature in my app. I have attach the working code of UIDocument and my Model class.
Thank you in advance !
UIDocument
class NoteDocument: UIDocument {
var notes = [Note]()
override func load(fromContents contents: Any, ofType typeName: String?) throws {
if let contents = contents as? Data {
if let arr = try? PropertyListDecoder().decode([Note].self, from: contents) {
self.notes = arr
return
}
}
//if we get here, there was some kind of problem
throw NSError(domain: "NoDataDomain", code: -1, userInfo: nil)
}
override func contents(forType typeName: String) throws -> Any {
if let data = try? PropertyListEncoder().encode(self.notes) {
return data
}
//if we get here, there was some kind of problem
throw NSError(domain: "NoDataDomain", code: -2, userInfo: nil)
}
}
Model
class Model: ObservableObject {
var document: NoteDocument?
var documentURL: URL?
init() {
let fm = FileManager.default
let driveURL = fm.url(forUbiquityContainerIdentifier: nil)?.appendingPathComponent("Documents")
documentURL = driveURL?.appendingPathComponent("savefile.txt")
document = NoteDocument(fileURL: documentURL!)
}
func loadData(viewModel: ViewModel) {
let fm = FileManager.default
if fm.fileExists(atPath: (documentURL?.path)!) {
document?.open(completionHandler: { (success: Bool) -> Void in
if success {
viewModel.notes = self.document?.notes ?? [Note]()
print("File load successfull")
} else {
print("File load failed")
}
})
} else {
document?.save(to: documentURL!, for: .forCreating, completionHandler: { (success: Bool) -> Void in
if success {
print("File create successfull")
} else {
print("File create failed")
}
})
}
}
func saveData(_ notes: [Note]) {
document!.notes = notes
document?.save(to: documentURL!, for: .forOverwriting, completionHandler: { (success: Bool) -> Void in
if success {
print("File save successfull")
} else {
print("File save failed")
}
})
}
func autoSave(_ notes: [Note]) {
document!.notes = notes
document?.updateChangeCount(.done)
}
}
Note
class Note: Identifiable, Codable {
var id = UUID()
var title = ""
var text = ""
}
This is a complex topic. Apple do provide some sample swift code, the Document-Based App Programming Guide for iOS and iCloud Design Guide.
There is also some good third party guidance: Mastering the iCloud Document Store.
I would recommend reading the above, and then return to the NSMetaDataQuery API. NSMetaDataQuery has an initial gathering phase and a live-update phase. The later phase can remain in operation for the lifetime of your app, allowing you to be notified of new documents in your app's iCloud container.
I'm trying to connect two players with each other using GameKit in a very simple game. I want to use GKMatchmaker.shared().findMatch as I don't want to show any GameCenter related view controllers. (to keep it simple)
Problem:
Even though GameKit creates a match after finding two players, an error occurs that prevents either player from sending any message to the others.
Current Situation:
The basic code is as follows (based on the docs described here: https://developer.apple.com/documentation/gamekit/finding_multiple_players_for_a_game)
print("Requesting multiplayer match")
let request = GKMatchRequest()
request.minPlayers = 2
request.maxPlayers = 2
request.recipientResponseHandler = {(player: GKPlayer, respnse: GKInviteRecipientResponse) -> Void in
print("new player about to join")
print(player.alias)
print(respnse)
}
GKMatchmaker.shared().findMatch(for: request, withCompletionHandler: {
(match: GKMatch?, error: Error?) -> Void in
if error != nil {
// Handle the error that occurred finding a match.
print("error during matchmaking")
print(error as Any)
} else if match != nil {
guard let match = match else { return }
print("connected to \(match.players.count) players")
// load the multiplayer data handler
let handler = MultiMatchHandler()
match.delegate = handler
// load the multiplayer service
let service = MultiMatchService(match: match)
service.sendMessageToAll(text: "Hello from the other side")
// finish the match making
GKMatchmaker.shared().finishMatchmaking(for: match)
// Start the game with the players in the match.
self.view?.presentScene(GameScene.newScene(multiplayer: service))
}
})
The output of that is
Requesting multiplayer match
2022-01-05 01:19:16.554959+0100 Grapefruit[38300:10026027] [Match] cannot set connecting state for players: (
"<GKPlayer: 0x282add280>(alias:... gamePlayerID:... teamPlayerID:... name:... status:(null) friendBiDirectional:0 friendPlayedWith:1 friendPlayedNearby:0 acceptedGameInviteFromThisFriend:0 initiatedGameInviteToThisFriend:0 automatchedTogether:1)"
), as there is no inviteDelegate set yet. The state might directly change to Ready when we set the inviteDelegate later and call sendQueuedStatesAndPackets.
2022-01-05 01:19:16.557002+0100 Grapefruit[38300:10026027] [Match] syncPlayers failed to loadPlayersForLegacyIdentifiers: (
"..."
)
connected to 0 players
sending text Hello from the other side failed
Findings:
minPlayers is set to 2. As the completion handler is called this means that at least one more player was found. But the number of players returned in match.players.count is 0
The matcher shows an error saying that cannot set connecting state for players ... as there is no inviteDelegate set yet. I can't find any info about this invite delegate.
Actual Question:
What is an inviteDelegate? Do I really need to implement such (if yes, then how?)? (I don't think so as the docs state that the match only starts after the invites are accepted).
How can I resolve this issue?
here is a working example for you. open on two machines, make sure both are authenticated, press "findMatch()" on both machines (and wait for confirmation), then ping baby ping
i believe the "no inviteDelegate set yet" error doesn't mean the match making necessary failed, and can safely be ignored, as mentioned here
you'll want to implement more of the GKMatchDelegate protocol, but this is a skeleton for demonstration purposes
import SwiftUI
import GameKit
import SpriteKit
class MyGameScene: SKScene, GKMatchDelegate {
override func didMove(to view: SKView) {
self.backgroundColor = .yellow
}
//GKMatchDelegate protocol
func match(_ match: GKMatch, didReceive data: Data, forRecipient recipient: GKPlayer, fromRemotePlayer player: GKPlayer) {
print("\(Self.self) \(#function) -- ping received")
}
}
struct Matchmaker: View {
#State var isAuthenticated:Bool = false
#State var scene = MyGameScene()
#State var match:GKMatch? = nil
var body: some View {
ZStack {
Color.clear
SpriteView(scene: scene)
VStack(alignment: .leading, spacing: 20) {
Text("1) authenticate() \(Image(systemName: isAuthenticated ? "checkmark.icloud" : "xmark.icloud"))")
Button { findMatch() } label: {
Text("2) findMatch() \(Image(systemName: (match != nil) ? "person.fill.checkmark" : "person.fill.xmark"))")
}
Button { ping() } label: {
Text("3) ping()")
}
}
}
.onAppear() {
authenticate()
}
}
func authenticate() {
GKLocalPlayer.local.authenticateHandler = { viewController, error in
if let error = error { print(error) }
isAuthenticated = (error == nil)
}
}
func findMatch() {
guard isAuthenticated else { return }
let request = GKMatchRequest()
request.minPlayers = 2
request.maxPlayers = 2
request.playerAttributes = 0xFFFFFFFF //mask for "i'll match with anyone"
GKMatchmaker.shared().findMatch (for: request) { match, error in
if let error = error { print(error) }
self.match = match
self.match?.delegate = scene
}
}
func ping() {
let players = match?.players ?? [];
let data = Data()
do {
try match?.send(data, to: players, dataMode: .reliable)
} catch {
print("Sending failed")
}
}
}
First of all I'm really new to Firestore and its functionalities, so I apologize if some of this might feel obvious to others. These things are just not registering in my mind yet.
I'm trying to create a 2 person multiplayer game using the language Swift and Firestore as the backend. However I'm not to sure how to create this functionality of only allowing two players inside a single game at a given time. How would I go about restricting each game to only allowing two players inside one game? Would this be something I need to set up within the security and rules portion of Firestore? Or would I need to create this functionality within how I model my data?
My current setup for how I'm modeling the data includes creating a collection of "Games" where each "Game" has two documents for "player1" and "player2". Then, within each one of those players/documents I store the values of each players functionalities. But with this approach, I still haven't solved the issue of only allowing two players within a single "Game"/collection. How do I prevent a third player from entering the game? or how would I handle the situation when more than one person enters a game at the same time?
Thank you for any advice possible.
You can use Cloud Functions to assign players to a game, manage when a game is full and then start it. Take a look at this article on Medium, Building a multi-player board game with Firebase Firestore & Functions
This is the code I ended up creating in Swift using Firestore to create the 2 person multiplayer game.
import UIKit
import Firebase
class WaitForOpponentViewController: UIViewController
{
#IBOutlet weak var activityIndicator: UIActivityIndicatorView!
var battleRoomDocumentReference : DocumentReference!
var battleRoomListenerRegistration : ListenerRegistration!
override func viewDidLoad(){
super.viewDidLoad()
activityIndicator.hidesWhenStopped = true
activityIndicator.startAnimating()
Firestore.firestore().collection(BATTLEROOMS_Collection)
.whereField(BATTLEROOMFULL_FIELD, isEqualTo: false)
.whereField(NUMOFPLAYERS_FIELD, isLessThan: 2)
.limit(to: 1)
.getDocuments { (snapShot, error) in
if let error = error
{
print("There was a error while fetching the documents: \(error)")
}
else
{
guard let snap = snapShot else {return}
if(snap.documents.count > 0)
{
//Update the current battle room
for document in snap.documents
{
Firestore.firestore().collection(BATTLEROOMS_Collection)
.document(document.documentID)
.setData(
[
BATTLEROOMFULL_FIELD : true,
NUMOFPLAYERS_FIELD : 2, //Note: Player1Id is not changed because there is already a player1Id when this document is updated
PLAYER2ID_FIELD : Auth.auth().currentUser?.uid ?? "AnonymousNum2"
], options: SetOptions.merge(), completion: { (error) in
if let error = error
{
print("There was an error while adding the second player to the battle room document : \(error)")
}
self.addBattleRoomListener(battleRoomDocumentId: document.documentID)
})
}
}
else
{
//Create a new battle room
self.battleRoomDocumentReference = Firestore.firestore().collection(BATTLEROOMS_Collection)
.addDocument(data:
[
BATTLEROOMFULL_FIELD: false,
NUMOFPLAYERS_FIELD : 1,
PLAYER1ID_FIELD : Auth.auth().currentUser?.uid ?? "AnonymousNum1",
PLAYER2ID_FIELD : ""
], completion: { (error) in
if let error = error
{
print("Error while adding a new battle room/player 1 to the battle room document : \(error)")
}
})
self.addBattleRoomListener(battleRoomDocumentId: self.battleRoomDocumentReference.documentID)
}
}
}
}
override func viewWillDisappear(_ animated: Bool) {
//Remove Battle Room Listener
battleRoomListenerRegistration.remove()
activityIndicator.stopAnimating()
}
func addBattleRoomListener(battleRoomDocumentId : String)
{
battleRoomListenerRegistration = Firestore.firestore().collection(BATTLEROOMS_Collection)
.document(battleRoomDocumentId)
.addSnapshotListener { (documentSnapshot, error) in
guard let snapshot = documentSnapshot else { return }
guard let documentData = snapshot.data() else { return }
let battleRoomFullData = documentData[BATTLEROOMFULL_FIELD] as? Bool ?? false
let numOfPlayerData = documentData[NUMOFPLAYERS_FIELD] as? Int ?? 0
if(battleRoomFullData == true && numOfPlayerData == 2)
{
print("Two Players in the Game, HURRAY. Segue to GAME VIEW CONTROLLER")
}
else
{
return
}
}
}
#IBAction func cancelBattle(_ sender: UIButton) {
//NOTE: Canceling is only allowed for the first user thats creates the Battle Room, once the Second Person enters the Battle Room the system will automatically segue to the Game View Controller sending both players into the game VC
Firestore.firestore().collection(BATTLEROOMS_Collection)
.document(battleRoomDocumentReference.documentID)
.delete { (error) in
if let error = error
{
print("There was an error while trying to delete a document: \(error)")
}
}
activityIndicator.stopAnimating()
self.dismiss(animated: true, completion: nil)
}
}
I'm trying to report my highscore in the game center. I think my code is working but the game center is not updating with the highscore.
The leaderboard is create with this reference name : "funfairBalloon" and this leaderboard ID : 55009943.
I have 3 sandbox testers, the game center is enable and the players are authenticated in game center.
and my code to authenticate and to report is :
func authenticateLocalPlayer()
{
var localPlayer = GKLocalPlayer.localPlayer()
localPlayer.authenticateHandler =
{ (viewController : UIViewController!, error : NSError!) -> Void in
if viewController != nil
{
self.presentViewController(viewController, animated:true, completion: nil)
}
else
{
if GKLocalPlayer.localPlayer().authenticated {
let gkScore = GKScore(leaderboardIdentifier: "55009943")
gkScore.value = Int64(highscore)
GKScore.reportScores([gkScore], withCompletionHandler: {(error) -> Void in
let alert = UIAlertView(title: "Success",
message: "Score updated",
delegate: self,
cancelButtonTitle: "Ok")
alert.show()
})
}
}
}
}
Do you have an idea?
It's best practice to add your app id to the leaderboard identifier. I had trouble not getting it to work before then. You may be having the same troubles. Make a test leaderboard named "com.whateverName.55009943" and update your code. See if that works like it did for me.
If you are using Test Flight for your sandbox testers make sure to add them on iTunes connect as well.
Finally, this link should help you troubleshoot why you're not seeing anyone show up on the leaderboard if you followed the above advice.
You can take a look at this logic in this github repo https://github.com/jocelynlih/SwiftGameBook/blob/master/PencilAdventure/PencilAdventure/ScoreManager.swift#L26
To report score you need to call pass the authenticateHandler closure function and in that if localPlayer is authenticated then report score.
var localPlayer = GKLocalPlayer.localPlayer()
localPlayer.authenticateHandler = {(viewController : UIViewController!, error : NSError!) -> Void in
if viewController != .None {
// Show view controller
} else {
if localPlayer.authenticated {
var scoreToReport = GKScore(leaderboardIdentifier: "Leaderboard\(level)", player: localPlayer)
scoreToReport.value = Int64(score)
GKScore.reportScores([scoreToReport], withCompletionHandler: nil)
} else {
// User not authenticated
}
}
}