GameKit Real Time Multiplayer Tutorial

Geovana Contine
Academy@EldoradoCPS
8 min readJul 1, 2020

O framework GameKit fornece uma série de funcionalidades destinada ao desenvolvimento de jogos, tais como Leaderboards, Achievements e Multiplayer. O último, embora extremamente prático para pequenos times, não é tão famoso como os demais, fato que reflete na idade avançada dos tutoriais disponíveis.

Com as mudanças que o WWDC20 trouxe para o Game Center e a consequente expectativa de que o uso da plataforma aumente, este tutorial tem como objetivo apresentar uma implementação básica de uma partida multiplayer em real time.

As seguintes etapas serão exploradas neste artigo:

  • Como adicionar o Game Center ao seu projeto
  • Como autenticar um jogador no Game Center
  • Como utilizar a ferramenta de procurar partida (Matchmaker)
  • Como funciona uma partida multiplayer
  • Criando o Game Model
  • Como enviar/receber dados entre os jogadores

Ao final dessas etapas teremos como resultado um jogo de dois jogadores, cada um controlando um personagem, onde podem realizar uma ação que é refletida na tela do outro. Adicionalmente, há um contador de tempo sincronizado entre os dois.

O código final está disponível em: https://github.com/pedrocontine/GameKitMultiplayerTutorial

Importante: Faça o download do material! Durante o artigo irei comentar algumas funções importantes, mas não entrarei em todos os detalhes da implementação. E claro, serão necessários dois devices para testar o projeto, sejam eles simuladores ou reais.

Como adicionar o Game Center

Clique no projeto -> Signing & Capabilities -> Game Center.

Depois vá até a App Store Connect e adicione um novo app. Na aba Features já será possível ver que o Game Center foi adicionado. Teoricamente apenas isso já seria o suficiente, mas as vezes é necessário criar um Leaderboard qualquer para que seu app seja reconhecido. Por via das dúvidas, crie um temporário para evitarmos problemas.

Caso tenha alguma dúvida nessa etapa, esse outro tutorial mais detalhado pode te ajudar.

Autenticação

Para criarmos uma partida online é necessário que os jogadores estejam conectados ao Game Center. Dentro da classe GameCenterHelper temos o método authenticatePlayer, que irá verificar se o jogador está conectado e caso não esteja nos retorna uma View Controller padrão de login para exibirmos. Não precisamos fazer mais nada, a etapa de login é toda interna do Game Center. É recomendado realizar essa autenticação o mais cedo possível dentro da sua aplicação, preferencialmente no AppDelegate. Uma vez autenticado, podemos acessar todas as informações disponíveis sobre o jogador através do GKLocalPlayer.local.

Login View Controller

Matchmaker

Assim como a autenticação, no nosso GameCenterHelper também temos o método presentMatchmaker, que nos permite passar a informação de quantos jogadores teremos na partida, bem como uma mensagem que será exibida quando um convite for enviado para um amigo. Isso também nos retorna uma View Controller padrão do Game Center, que permite ao usuário convidar amigos e encontrar adversários aleatórios automaticamente, tudo sem precisarmos fazer absolutamente nada.

extension GameCenterHelper: GKMatchmakerViewControllerDelegate {
func matchmakerViewController(_ viewController: GKMatchmakerViewController, didFind match: GKMatch) {
viewController.dismiss(animated: true)
delegate?.presentGame(match: match)
}
}

Por fim, através do GKMatchmakerViewControllerDelegate, recebemos um GKMatch quando uma partida for criada.

View Controller de matchmaker retornada pelo Game Center

Como funciona uma partida multiplayer

Até agora todo nosso código estava concentrado no GameCenterHelper, implementado pelo MenuViewController, justamente pois são etapas obrigatórias e nosso único trabalho é exibir as telas que o Game Center já nos disponibiliza. Porém, agora que geramos uma partida, vamos passá-la para nossa GameViewController, que será a responsável por gerenciar nosso jogo.

Antes de tudo, precisamos entender a lógica de um jogo multiplayer. Nós temos um objeto que irá guardar todas as informações essenciais pro jogo, como vida dos jogadores, tempo de partida, etc. Cada jogador terá um objeto desse e sua tela será carregada conforme essas informações. Para que todos vejam as mesmas coisas, todos os objetos devem ser iguais.

Cada ação que o jogador fizer, como atacar ou se mover, irá alterar somente as informações do seu objeto e por consequência o seu próprio jogo. Portanto, o jogador envia esse objeto com as mudanças para todos os outros, e eles substituem os que já tinham pelo recebido. Como toda mudança nesse objeto vai atualizar a tela, todos verão as mesmas coisas. Assim, basicamente a essência do nosso jogo será enviar e receber esse objeto.

Game Model

Nosso objeto a ser compartilhado é essa struct que se chama GameModel. Ela contem o tempo restante e uma lista de jogadores, que possuem nome, um estado de animação e a vida restante. Como dito anteriormente, você pode colocar o que precisar nesse modelo, único requisito é que ele possa ser convertido para o tipo Data e por isso precisa ser Codable.

import Foundation
import UIKit
import GameKit

struct GameModel: Codable {
var players: [Player] = []
var time: Int = 60
}

extension GameModel {
func encode() -> Data? {
return try? JSONEncoder().encode(self)
}

static func decode(data: Data) -> GameModel? {
return try? JSONDecoder().decode(GameModel.self, from: data)
}
}
struct Player: Codable {
var displayName: String
var status: PlayerStatus = .idle
var life: Float = 100
}

Note que todos os elementos visuais da nossa tela que são dinâmicos se atualizam conforme os valores contidos no GameModel. Inclusive, com o didSet garantimos que qualquer mudança no nosso modelo irá chamar a função updateUI, responsável por atualizar todos os valores do jogo.

private var gameModel: GameModel! {
didSet {
updateUI()
}
}

Enviar/Receber data

Com nosso GameModel definido e toda nossa interface se atualizando a partir dele, chegou a hora de enviá-lo aos demais jogadores quando houver alguma alteração. Utilizamos o método sendData disponível no GKMatch, passando o GameModel convertido em Data.

private func sendData() {
guard let match = match else { return }

do {
guard let data = gameModel.encode() else { return }
try match.sendData(toAllPlayers: data, with: .reliable)
} catch {
print("Send data failed")
}
}

Quando essa informação for recebida, é disparado o método didReceive data do GKMatchDelegate , onde os outros jogadores receberão o GameModel. Após receber o novo modelo, basta substituir o atual pelo novo. É apenas isso o necessário para realizar a troca de informação que comentamos anteriormente.

extension GameViewController: GKMatchDelegate {
func match(_ match: GKMatch, didReceive data: Data, fromRemotePlayer player: GKPlayer) {
guard let model = GameModel.decode(data: data) else { return }
gameModel = model
}
}

Identificar jogadores

Já temos os usuários autenticados, dentro de uma partida e sendo capazes de comunicar alterações no GameModel. No nosso jogo de exemplo temos dois lutadores, cada jogador responsável por controlar apenas um. Para isso, precisamos definir qual usuário conectado será o player1 e qual será o player2.

O GKMatch possui a lista de jogadores conectados, mas infelizmente ela contém todos os jogadores exceto o usuário local. A solução que eu encontrei foi criar o meu próprio array de jogadores, salvá-lo no GameModel e utilizar o índex que eles ocupam no array para definir qual player eles serão. Esse processo está no método savePlayers.

private func savePlayers() {
guard let player2Name = match?.players.first?.displayName else { return }

let player1 = Player(displayName: GKLocalPlayer.local.displayName)
let player2 = Player(displayName: player2Name)

gameModel.players = [player1, player2]

gameModel.players.sort { (player1, player2) -> Bool in
player1.displayName < player2.displayName
}

sendData()
}

Isso gera algumas dificuldades como garantir que todos os usuários gerem uma lista idêntica, entre outras complicações que você encontrará se seguir o mesmo raciocínio. Imagino que exista uma maneira melhor de fazer isso, caso a encontre deixe nos comentários :)

Embora não pareça a solução ideal, com isso já somos capazes de definir quem será o dinossauro lutador azul e quem será o vermelho. O método getLocalPlayerType permite identificar qual desses dois o jogador local é e consequentemente alterar a cor do botão de atacar.

private func getLocalPlayerType() -> PlayerType {
if gameModel.players.first?.displayName == GKLocalPlayer.local.displayName {
return .one
} else {
return .two
}
}

Exemplo Ataque

Ao clicar para atacar, identificamos qual é o atacante e naturalmente quem será o atacado. Alteramos o status de animação dos jogadores, atualizamos a vida do jogador atacado e tudo o mais que for necessário para o nosso combate. Por fim é só chamar o sendData para comunicar ao outro jogador as alterações.

@IBAction func buttonAttackPressed() {
let localPlayer = getLocalPlayerType()

gameModel.players[localPlayer.index()].status = .attack
gameModel.players[localPlayer.enemyIndex()].status = .hit
gameModel.players[localPlayer.enemyIndex()].life -= 10
sendData()

DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.gameModel.players[localPlayer.index()].status = .idle
self.gameModel.players[localPlayer.enemyIndex()].status = .idle
self.sendData()
}
}

O ataque é um exemplo de método que ambos os jogadores poderão chamar. Perceba que a dificuldade consiste em escrever o código para que ele funcione independente de qual é o jogador.

Exemplo Relógio

Diferentemente do ataque, o relógio pode não ser uma boa ideia deixar que os dois jogadores atualizem. Nesse exemplo definimos que apenas o player1 irá iniciar o timer, e sempre que se passar 1 segundo o novo tempo restante é enviado ao outro jogador. Esse é um exemplo de atualização realizada de maneira independente, sem a necessidade de uma ação do usuário.

private func initTimer() {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { (timer) in
let player = self.getLocalPlayerType()
if player == .one, self.gameModel.time >= 1 {
self.gameModel.time -= 1
self.sendData()
}
})
}

E o lag?

Até aqui tudo é lindo. Se estiver testando com dois simuladores a atualização das informações é quase instantânea, tudo funciona perfeitamente. Mas sabemos que isso dificilmente será sempre assim. Em uma situação real os usuários terão diferenças na qualidade da conexão de internet e essas transições do nosso GameModel terão um pequeno atraso. Isso é inevitável e todos os jogos online sofrem disso, é o famoso lag. Contudo, existem técnicas para amenizar os estragos dessa situação. Este é um artigo muito interessante caso queira se aprofundar.

Game Over

Espero que o código disponibilizado tenha ajudado a entender um pouco como funciona a dinâmica de um jogo multiplayer. Explore um pouco os delegates do GKMatch e do matchmaker e você verá que tem muuuuitas funcionalidades extras que eu não comentei, como canal de voz, tratamento de erros, etc.

Como dito, esse é um campo onde poucos tutoriais estão disponíveis, então qualquer contribuição que queira dar aqui será totalmente bem vinda :)

--

--