0

I am writing several Swift multiplayer games based on the Ray Wenderlich tutorial for Nine Knights. (https://www.raywenderlich.com/7544-game-center-for-ios-building-a-turn-based-game)

I use pretty much the same GameCenterHelper file except that I change to a segue instead of present scene since I am using UIKit instead of Sprite Kit with the following important pieces:

present match maker:

func presentMatchmaker() {
guard GKLocalPlayer.local.isAuthenticated else {return}

let request = GKMatchRequest()

request.minPlayers = 2
request.maxPlayers = 2
request.inviteMessage = "Would you like to play?"

let vc = GKTurnBasedMatchmakerViewController(matchRequest: request)
vc.turnBasedMatchmakerDelegate = self

currentMatchmakerVC = vc
print(vc)
viewController?.present(vc, animated: true)
}

the player listener function:

extension GameCenterHelper: GKLocalPlayerListener {
func player(_ player: GKPlayer, receivedTurnEventFor match: GKTurnBasedMatch, didBecomeActive: Bool) {
if let vc = currentMatchmakerVC {
  currentMatchmakerVC = nil
  vc.dismiss(animated: true)
}

guard didBecomeActive else {return}

NotificationCenter.default.post(name: .presentGame, object: match)
}
}

The following extension for Notification Center:

extension Notification.Name {
static let presentGame = Notification.Name(rawValue: "presentGame")
static let authenticationChanged = Notification.Name(rawValue: "authenticationChanged")
}

In the viewdidload of the menu I call the following:

override func viewDidLoad() {
    super.viewDidLoad()
    createTitleLabel()
    createGameImage()
    createButtons()
    
    GameCenterHelper.helper.viewController = self
    GameCenterHelper.helper.currentMatch = nil
    
    NotificationCenter.default.addObserver(self, selector: #selector(authenticationChanged(_:)), name: .authenticationChanged, object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(presentGame(_:)), name: .presentGame, object: nil)
}

and tapping the multi device buttons calls the following:

@objc func startMultiDeviceGame() {
    multiPlayer = true
    GameCenterHelper.helper.presentMatchmaker()
}

and the notifications call the following:

@objc func presentGame(_ notification: Notification) {
  // 1
    print("present game")
  guard let match = notification.object as? GKTurnBasedMatch else {return}
  
  loadAndDisplay(match: match)
}

// MARK: - Helpers
private func loadAndDisplay(match: GKTurnBasedMatch) {
    match.loadMatchData { [self] data, error in
    if let data = data {
      do {
        gameModel = try JSONDecoder().decode(GameModel.self, from: data)
      } catch {gameModel = GameModel()}
    } else {gameModel = GameModel()}
    
    GameCenterHelper.helper.currentMatch = match
        print("load and display")
    performSegue(withIdentifier: "gameSegue", sender: nil)
  }
}

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    print("prepare to segue")
    if let vc = segue.destination as? GameVC {vc.gameModel = gameModel}
}

Which is hopefully easy to follow.

  1. The game starts and the menu scene adds the observer for present game
  2. The player taps multi device, which presents the matchmaker
  3. The player selects their game from the match maker, which I think activates the player listener function
  4. This posts to the Notification Center for present game
  5. The notification center observer calls present game, which calls load and display, with a little help from prepare segue

My issue is that the first time I do this it works perfectly, and per the framework from that tutorial that I can't figure out how to change (an issue for a different question I think) after a player takes their turn they are returned to the menu. The second time they enter present matchmaker and select a game the present game function is called twice, and the third time they take their turn without shutting down the app it is called 3 times, etc. (I have the print statements in both the present game and load and display functions and they are called back to back the 2nd time through and back to back to back the 3rd time etc. even though they are only called once the first time a game is selected from the matchmaker)

Console messages

present matchmaker true
<GKTurnBasedMatchmakerViewController: 0x104810000>
present game
present game
present game
load and display
prepare to segue
load and display
prepare to segue
load and display
prepare to segue
2021-03-20 22:32:26.838680-0600 STAX[4997:435032] [Presentation] Attempt to present <STAX.GameVC: 0x103894c00> on <Game.MenuVC: 0x103814800> (from < Game.MenuVC: 0x103814800>) whose view is not in the window hierarchy.
(419.60100000000006, 39.0)
2021-03-20 22:32:26.877943-0600 STAX[4997:435032] [Presentation] Attempt to present <STAX.GameVC: 0x103898e00> on < Game.MenuVC: 0x10501c800> (from < Game.MenuVC: 0x10501c800>) whose view is not in the window hierarchy.

I had thought that this was due to me not removing the Notification Center observers, but I tried the following in the view did load for the menu screen (right before I added the .presentGame observer):

NotificationCenter.default.removeObserver(self, name: .presentGame, object: nil)

and that didn't fix the issue, so I tried the following (in place of the above):

NotificationCenter.default.removeObserver(self)

and that didn't work so I tried them each, one at a time in the view did disappear of the game view controller (which I didn't think would work since self refers to the menu vc, but I was getting desperate) and that didn't work either.

I started thinking that maybe I'm not adding multiple observers that are calling present game more than once, since the following didn't work at all the second time (I'm just using a global variable to keep track of the first run through that adds the observers and then not adding them the second time):

if addObservers {
    NotificationCenter.default.addObserver(self, selector: #selector(authenticationChanged(_:)), name: .authenticationChanged, object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(presentGame(_:)), name: .presentGame, object: nil)
        addObservers = false
    }

since it is trying to add a view that is not in the view hierarchy. (Although the background music for that screen starts playing, the menu remains and the game board is not shown...)

I wasn't sure if I'm removing the Notification Center observers incorrectly or if they aren't really the source of the problem so I decided to ask for help :)

Thank you!

Dominick
  • 164
  • 2
  • 4
  • 13

1 Answers1

0

I figured it out. I was trying to remove the Notifications from a deallocated instance of the view controller per the below link (The bottom most answer):

How to avoid adding multiple NSNotification observer?

The correct way to remove the notifications is in the view will disappear function like this:

override func viewWillDisappear(_ animated: Bool) {
        NotificationCenter.default.removeObserver(self, name: Notification.Name.presentGame, object: nil)
        NotificationCenter.default.removeObserver(self, name: Notification.Name.authenticationChanged, object: nil)
    }

After implementing that I stopped making multiple calls to the notification center.

Dominick
  • 164
  • 2
  • 4
  • 13