Suporte a Jogos salvos no Android Games

Este guia mostra como implementar um jogo salvo usando a API de snapshots fornecida pelos serviços relacionados a jogos do Google Play. As APIs podem ser encontradas nos pacotes com.google.android.gms.games.snapshot e com.google.android.gms.games.

Antes de começar

Caso ainda não tenha feito isso, recomendamos consultar os conceitos de jogos salvos.

Como conseguir o cliente de snapshots

Para começar a usar a API de snapshots, o jogo precisa ter um objeto SnapshotsClient. Para isso, chame o método Games.getSnapshotsClient() e transmita a atividade.

Especificar o escopo do Drive

A API de snapshots depende da API Google Drive para armazenar os jogos salvos. Para acessar a API Drive, seu app precisa especificar o escopo Drive.SCOPE_APPFOLDER ao criar o cliente de login do Google.

Veja um exemplo de como fazer isso no método onResume() para sua atividade de login:


@Override
protected void onResume() {
  super.onResume();
  signInSilently();
}

private void signInSilently() {
  GoogleSignInOptions signInOption =
      new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_GAMES_SIGN_IN)
          // Add the APPFOLDER scope for Snapshot support.
          .requestScopes(Drive.SCOPE_APPFOLDER)
          .build();

  GoogleSignInClient signInClient = GoogleSignIn.getClient(this, signInOption);
  signInClient.silentSignIn().addOnCompleteListener(this,
      new OnCompleteListener<GoogleSignInAccount>() {
        @Override
        public void onComplete(@NonNull Task<GoogleSignInAccount> task) {
          if (task.isSuccessful()) {
            onConnected(task.getResult());
          } else {
            // Player will need to sign-in explicitly using via UI
          }
        }
      });
}

Mostrando jogos salvos

É possível integrar a API de snapshots sempre que o jogo fornecer aos jogadores a opção de salvar ou restaurar o progresso deles. O jogo pode exibir essa opção em pontos designados de salvamento/restauração ou permitir que os jogadores salvem ou restaurem o progresso a qualquer momento.

Depois que os jogadores selecionam a opção de salvar/restaurar o jogo, uma tela pode ser exibida, solicitando a inserção de informações de um novo jogo salvo ou a seleção de um jogo salvo para ser restaurado.

Para simplificar o desenvolvimento, a API de snapshots fornece uma interface do usuário (IU) de seleção padrão de jogos salvos que pode ser usada imediatamente. A IU de seleção de jogos salvos permite que os jogadores criem um novo jogo salvo, visualizem detalhes sobre os já existentes e carreguem os anteriores.

Para iniciar a IU padrão de Jogos salvos.

  1. Chame SnapshotsClient.getSelectSnapshotIntent() para receber uma Intent e iniciar a IU de seleção padrão de jogos salvos.
  2. Chame startActivityForResult() e transmita essa propriedade Intent. Se a chamada for bem-sucedida, o jogo vai exibir a IU da seleção de jogos salvos e as opções especificadas.

Confira um exemplo de como iniciar a IU padrão de seleção de jogos salvos:

private static final int RC_SAVED_GAMES = 9009;

private void showSavedGamesUI() {
  SnapshotsClient snapshotsClient =
      PlayGames.getSnapshotsClient(this);
  int maxNumberOfSavedGamesToShow = 5;

  Task<Intent> intentTask = snapshotsClient.getSelectSnapshotIntent(
      "See My Saves", true, true, maxNumberOfSavedGamesToShow);

  intentTask.addOnSuccessListener(new OnSuccessListener<Intent>() {
    @Override
    public void onSuccess(Intent intent) {
      startActivityForResult(intent, RC_SAVED_GAMES);
    }
  });
}

Se o jogador criar um novo jogo salvo ou carregar um jogo já salvo, a IU vai enviar uma solicitação aos serviços relacionados a jogos do Google Play. Se a solicitação for bem-sucedida, os serviços relacionados a jogos do Google Play vão retornar informações para criar ou restaurar o jogo salvo usando o callback onActivityResult(). Seu jogo pode substituir esse callback para verificar se ocorreram erros durante a solicitação.

O snippet de código a seguir mostra um exemplo de implementação de onActivityResult():

private String mCurrentSaveName = "snapshotTemp";

/**
 * This callback will be triggered after you call startActivityForResult from the
 * showSavedGamesUI method.
 */
@Override
protected void onActivityResult(int requestCode, int resultCode,
                                Intent intent) {
  if (intent != null) {
    if (intent.hasExtra(SnapshotsClient.EXTRA_SNAPSHOT_METADATA)) {
      // Load a snapshot.
      SnapshotMetadata snapshotMetadata =
          intent.getParcelableExtra(SnapshotsClient.EXTRA_SNAPSHOT_METADATA);
      mCurrentSaveName = snapshotMetadata.getUniqueName();

      // Load the game data from the Snapshot
      // ...
    } else if (intent.hasExtra(SnapshotsClient.EXTRA_SNAPSHOT_NEW)) {
      // Create a new snapshot named with a unique string
      String unique = new BigInteger(281, new Random()).toString(13);
      mCurrentSaveName = "snapshotTemp-" + unique;

      // Create the new snapshot
      // ...
    }
  }
}

Como escrever jogos salvos

Para armazenar conteúdo em um jogo salvo, faça o seguinte:

  1. Abra um snapshot de forma assíncrona usando SnapshotsClient.open(). Em seguida, recupere o objeto Snapshot do resultado da tarefa chamando SnapshotsClient.DataOrConflict.getData().
  2. Recupere uma instância SnapshotContents via SnapshotsClient.SnapshotConflict.
  3. Chame SnapshotContents.writeBytes() para armazenar os dados do jogador em formato de bytes.
  4. Depois de gravar todas as mudanças, chame SnapshotsClient.commitAndClose() para enviá-las aos servidores do Google. Na chamada do método, o jogo pode fornecer mais informações para informar aos serviços relacionados a jogos do Google Play como apresentar esse jogo salvo aos jogadores. Essas informações são representadas por um objeto SnapshotMetaDataChange, que o jogo cria usando SnapshotMetadataChange.Builder.

O snippet a seguir mostra como o jogo pode confirmar mudanças em um jogo salvo:

private Task<SnapshotMetadata> writeSnapshot(Snapshot snapshot,
                                             byte[] data, Bitmap coverImage, String desc) {

  // Set the data payload for the snapshot
  snapshot.getSnapshotContents().writeBytes(data);

  // Create the change operation
  SnapshotMetadataChange metadataChange = new SnapshotMetadataChange.Builder()
      .setCoverImage(coverImage)
      .setDescription(desc)
      .build();

  SnapshotsClient snapshotsClient =
      PlayGames.getSnapshotsClient(this);

  // Commit the operation
  return snapshotsClient.commitAndClose(snapshot, metadataChange);
}

Se o dispositivo do jogador não estiver conectado a uma rede quando o app chamar SnapshotsClient.commitAndClose(), os serviços relacionados a jogos do Google Play vão armazenar os dados de jogos salvos localmente no dispositivo. Após a reconexão do dispositivo, os serviços relacionados a jogos do Google Play sincronizam as mudanças no jogo salvo localmente em cache com os servidores do Google.

Carregando jogos salvos

Para recuperar jogos salvos do jogador conectado no momento:

  1. Abra um snapshot de forma assíncrona usando SnapshotsClient.open(). Em seguida, recupere o objeto Snapshot do resultado da tarefa chamando SnapshotsClient.DataOrConflict.getData(). Como alternativa, seu jogo também pode extrair um snapshot específico por meio da IU de seleção de jogos salvos, conforme descrito em Como exibir Jogos salvos.
  2. Recupere a instância SnapshotContents via SnapshotsClient.SnapshotConflict.
  3. Chame SnapshotContents.readFully() para ler o conteúdo do snapshot.

O snippet a seguir mostra como carregar um jogo salvo específico:

Task<byte[]> loadSnapshot() {
  // Display a progress dialog
  // ...

  // Get the SnapshotsClient from the signed in account.
  SnapshotsClient snapshotsClient =
      PlayGames.getSnapshotsClient(this);

  // In the case of a conflict, the most recently modified version of this snapshot will be used.
  int conflictResolutionPolicy = SnapshotsClient.RESOLUTION_POLICY_MOST_RECENTLY_MODIFIED;

  // Open the saved game using its name.
  return snapshotsClient.open(mCurrentSaveName, true, conflictResolutionPolicy)
      .addOnFailureListener(new OnFailureListener() {
        @Override
        public void onFailure(@NonNull Exception e) {
          Log.e(TAG, "Error while opening Snapshot.", e);
        }
      }).continueWith(new Continuation<SnapshotsClient.DataOrConflict<Snapshot>, byte[]>() {
        @Override
        public byte[] then(@NonNull Task<SnapshotsClient.DataOrConflict<Snapshot>> task) throws Exception {
          Snapshot snapshot = task.getResult().getData();

          // Opening the snapshot was a success and any conflicts have been resolved.
          try {
            // Extract the raw data from the snapshot.
            return snapshot.getSnapshotContents().readFully();
          } catch (IOException e) {
            Log.e(TAG, "Error while reading Snapshot.", e);
          }

          return null;
        }
      }).addOnCompleteListener(new OnCompleteListener<byte[]>() {
        @Override
        public void onComplete(@NonNull Task<byte[]> task) {
          // Dismiss progress dialog and reflect the changes in the UI when complete.
          // ...
        }
      });
}

Como lidar com conflitos de jogos salvos

Ao usar a API de snapshots no seu jogo, é possível que vários dispositivos realizem leituras e gravações no mesmo jogo salvo. No caso de um dispositivo perder temporariamente a conexão de rede e depois se reconectar, isso pode causar conflitos de dados em que o jogo salvo no dispositivo local de um jogador esteja dessincronizado com a versão remota armazenada nos servidores do Google.

A API de snapshots fornece um mecanismo de resolução de conflitos que apresenta os dois conjuntos de jogos salvos conflitantes no tempo de leitura, além de permitir que você implemente uma estratégia de resolução adequada para o jogo.

Quando os serviços relacionados a jogos do Google Play detectam um conflito de dados, o método SnapshotsClient.DataOrConflict.isConflict() retorna um valor de true. Nesse caso, a classe SnapshotsClient.SnapshotConflict fornece duas versões do jogo salvo:

  • Versão do servidor: a versão mais atualizada conhecida pelos serviços relacionados a jogos do Google Play como sendo precisa para o dispositivo do jogador.
  • Versão local: é uma versão modificada detectada em um dos dispositivos do jogador que contém conteúdo ou metadados conflitantes. Essa versão pode não ser igual à que você tentou salvar.

Seu jogo precisa decidir como resolver o conflito, escolhendo uma das versões fornecidas ou mesclando os dados das duas versões do jogo salvo.

Para detectar e resolver conflitos de jogos salvos:

  1. Chame SnapshotsClient.open(). O resultado da tarefa contém uma classe SnapshotsClient.DataOrConflict.
  2. Chame o método SnapshotsClient.DataOrConflict.isConflict(). Se o resultado for verdadeiro, há um conflito para resolver.
  3. Chame SnapshotsClient.DataOrConflict.getConflict() para recuperar uma instância de SnaphotsClient.snapshotConflict.
  4. Chame SnapshotsClient.SnapshotConflict.getConflictId() para recuperar o ID do conflito que identifica exclusivamente o conflito detectado. O jogo precisa desse valor para enviar uma solicitação de resolução de conflitos mais tarde.
  5. Chame SnapshotsClient.SnapshotConflict.getConflictingSnapshot() para acessar a versão local.
  6. Chame SnapshotsClient.SnapshotConflict.getSnapshot() para acessar a versão do servidor.
  7. Para resolver o conflito do jogo salvo, selecione uma versão que você quer salvar no servidor como a versão final e a transmita para o método SnapshotsClient.resolveConflict().

O snippet a seguir mostra um exemplo de como seu jogo pode lidar com um conflito, selecionando o jogo salvo modificado mais recentemente como a versão final a ser salva:


private static final int MAX_SNAPSHOT_RESOLVE_RETRIES = 10;

Task<Snapshot> processSnapshotOpenResult(SnapshotsClient.DataOrConflict<Snapshot> result,
                                         final int retryCount) {

  if (!result.isConflict()) {
    // There was no conflict, so return the result of the source.
    TaskCompletionSource<Snapshot> source = new TaskCompletionSource<>();
    source.setResult(result.getData());
    return source.getTask();
  }

  // There was a conflict.  Try resolving it by selecting the newest of the conflicting snapshots.
  // This is the same as using RESOLUTION_POLICY_MOST_RECENTLY_MODIFIED as a conflict resolution
  // policy, but we are implementing it as an example of a manual resolution.
  // One option is to present a UI to the user to choose which snapshot to resolve.
  SnapshotsClient.SnapshotConflict conflict = result.getConflict();

  Snapshot snapshot = conflict.getSnapshot();
  Snapshot conflictSnapshot = conflict.getConflictingSnapshot();

  // Resolve between conflicts by selecting the newest of the conflicting snapshots.
  Snapshot resolvedSnapshot = snapshot;

  if (snapshot.getMetadata().getLastModifiedTimestamp() <
      conflictSnapshot.getMetadata().getLastModifiedTimestamp()) {
    resolvedSnapshot = conflictSnapshot;
  }

  return PlayGames.getSnapshotsClient(theActivity)
      .resolveConflict(conflict.getConflictId(), resolvedSnapshot)
      .continueWithTask(
          new Continuation<
              SnapshotsClient.DataOrConflict<Snapshot>,
              Task<Snapshot>>() {
            @Override
            public Task<Snapshot> then(
                @NonNull Task<SnapshotsClient.DataOrConflict<Snapshot>> task)
                throws Exception {
              // Resolving the conflict may cause another conflict,
              // so recurse and try another resolution.
              if (retryCount < MAX_SNAPSHOT_RESOLVE_RETRIES) {
                return processSnapshotOpenResult(task.getResult(), retryCount + 1);
              } else {
                throw new Exception("Could not resolve snapshot conflicts");
              }
            }
          });
}

Modificar jogos salvos para resolução de conflitos

Se você quiser mesclar dados de vários jogos salvos ou modificar um Snapshot existente para salvá-lo no servidor como a versão final resolvida, siga estas etapas:

  1. Chame SnapshotsClient.open() .
  2. Chame SnapshotsClient.SnapshotConflict.getResolutionSnapshotsContent() para receber um novo objeto SnapshotContents.
  3. Mescle os dados de SnapshotsClient.SnapshotConflict.getConflictingSnapshot() e SnapshotsClient.SnapshotConflict.getSnapshot() no objeto SnapshotContents da etapa anterior.
  4. Se quiser, crie uma instância SnapshotMetadataChange se houver mudanças nos campos de metadados.
  5. Chame SnapshotsClient.resolveConflict(). Na chamada de método, transmita SnapshotsClient.SnapshotConflict.getConflictId() como o primeiro argumento e os objetos SnapshotMetadataChange e SnapshotContents que você modificou anteriormente como o segundo e o terceiro argumentos, respectivamente.
  6. Se a chamada SnapshotsClient.resolveConflict() for bem-sucedida, a API armazenará o objeto Snapshot no servidor e tentará abrir o objeto Snapshot no dispositivo local.