Compatibilidad con juegos guardados en juegos para Android

En esta guía, se muestra cómo implementar juegos guardados con la API de instantáneas que proporcionan los Servicios de juego de Google Play. Se pueden encontrar las API en los paquetes com.google.android.gms.games.snapshot y com.google.android.gms.games.

Antes de comenzar

Si aún no lo hiciste, puede resultarte útil revisar los conceptos de juegos guardados.

Obtén el cliente de instantáneas

Para comenzar a usar la API de snapshots, el juego primero debe obtener un objeto SnapshotsClient. Para ello, llama al método Games.getSnapshotsClient() y pasa la actividad.

Especifica el alcance de Drive

La API de snapshots se basa en la API de Google Drive para el almacenamiento de juegos guardados. Para acceder a la API de Drive, la app debe especificar el alcance de Drive.SCOPE_APPFOLDER cuando se compila el cliente de Acceso con Google.

Este es un ejemplo de cómo hacerlo en el método onResume() para tu actividad de acceso:


@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
          }
        }
      });
}

Se muestran los juegos guardados

Puedes integrar la API de snapshots en cualquier lugar que tu juego les brinde a los jugadores la opción de guardar o restablecer su progreso. Tu juego puede mostrar esta opción en los puntos designados de guardar/restablecer o permitir que los jugadores guarden o restablezcan el progreso en cualquier momento.

Una vez que los jugadores seleccionen la opción de guardar/restablecer el juego, este podrá mostrar, de manera opcional, una pantalla que les pedirá que introduzcan la información para un nuevo juego guardado o que seleccionen uno existente a fin de restablecerlo.

A fin de simplificar el desarrollo, la API de snapshots proporciona una interfaz de usuario (IU) de selección de juegos guardados predeterminada que está lista para usar. La IU selección de juegos guardados permite que los jugadores creen un nuevo juego guardado, consulten los detalles de los juegos guardados existentes y carguen los juegos guardados anteriores.

Para iniciar la IU predeterminada de juegos guardados, haz lo siguiente:

  1. Llama a SnapshotsClient.getSelectSnapshotIntent() a fin de obtener un Intent para iniciar la IU de selección de juegos guardados predeterminada.
  2. Llama a startActivityForResult() y pasa ese Intent. Si la llamada se realiza de forma correcta, el juego muestra la IU de selección de juegos guardados, junto con las opciones que especificaste.

Este es un ejemplo de cómo iniciar la IU de selección de juegos guardados predeterminada:

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);
    }
  });
}

Si el jugador elige crear un juego guardado nuevo o cargar uno existente, la IU envía una solicitud a los Servicios de juego de Google Play. Si la solicitud se completa de forma correcta, los Servicios de juego de Google Play mostrarán información para crear o restablecer el juego guardado a través de la devolución de llamada onActivityResult(). Tu juego puede anular esta devolución de llamada para comprobar si se produjeron errores durante la solicitud.

El siguiente fragmento de código muestra una implementación de muestra 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
      // ...
    }
  }
}

Cómo escribir juegos guardados

Para almacenar contenido en un juego guardado, haz lo siguiente:

  1. Abre una instantánea de manera asíncrona a través de SnapshotsClient.open(). Luego, para recuperar el objeto Snapshot del resultado de la tarea, llama a SnapshotsClient.DataOrConflict.getData().
  2. Recupera una instancia de SnapshotContents a través de SnapshotsClient.SnapshotConflict.
  3. Llama a SnapshotContents.writeBytes() para almacenar los datos del reproductor en formato de bytes.
  4. Una vez que todos los cambios estén escritos, llama a SnapshotsClient.commitAndClose() para enviarlos a los servidores de Google. En la llamada de método, el juego puede proporcionar, de manera opcional, información adicional para indicarles a los Servicios de juego de Google Play cómo presentar este juego guardado a los jugadores. Esta información se representa en un objeto SnapshotMetaDataChange, que el juego crea mediante SnapshotMetadataChange.Builder.

En el siguiente fragmento, se muestra cómo tu juego puede confirmar los cambios en un juego guardado:

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);
}

Si el dispositivo del jugador no está conectado a una red cuando tu app llama a SnapshotsClient.commitAndClose(), los Servicios de juego de Google Play almacenan los datos del juego guardado de forma local en el dispositivo. Cuando se vuelve a conectar el dispositivo, los Servicios de juego de Google Play sincronizan los cambios del juego guardado en la caché local con los servidores de Google.

Cargando juegos guardados

Si deseas recuperar los juegos guardados para el jugador actualmente conectado, haz lo siguiente:

  1. Abre una instantánea de manera asíncrona a través de SnapshotsClient.open(). Luego, para recuperar el objeto Snapshot del resultado de la tarea, llama a SnapshotsClient.DataOrConflict.getData(). Como alternativa, tu juego también puede recuperar una instantánea específica a través de la IU de selección de juegos guardados, como se describe en Cómo mostrar los juegos guardados.
  2. Recupera la instancia SnapshotContents a través de SnapshotsClient.SnapshotConflict.
  3. Llama a SnapshotContents.readFully() para leer el contenido de la instantánea.

En el siguiente fragmento, se muestra cómo podrías cargar un juego guardado 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.
          // ...
        }
      });
}

Cómo administrar los conflictos de los juegos guardados

Cuando se utiliza la API de snapshots en el juego, es posible que varios dispositivos realicen operaciones de lectura y escritura en el mismo juego guardado. En el caso de que un dispositivo pierda, de forma temporal, su conexión de red y se vuelva a conectar más tarde, se podrían producir conflictos de datos por los que el juego guardado que se almacenó en el dispositivo local del jugador no se sincronice con la versión remota que se almacenó en los servidores de Google.

La API de snapshots proporciona un mecanismo de resolución de conflictos que presenta ambos conjuntos de juegos guardados en conflicto en el momento de la lectura y te permite implementar una estrategia de resolución adecuada para tu juego.

Cuando los Servicios de juego de Google Play detectan un conflicto de datos, el método SnapshotsClient.DataOrConflict.isConflict() muestra un valor de true. En este evento, la clase SnapshotsClient.SnapshotConflict proporciona dos versiones del juego guardado:

  • Versión del servidor: La versión más actualizada que conocen los Servicios de juego de Google Play para que sea precisa en función del dispositivo del jugador.
  • Versión local: Una versión modificada que se detecta en uno de los dispositivos del reproductor y que tiene contenido o metadatos en conflicto. Es posible que no sea la misma que la versión que intentaste guardar.

Tu juego debe decidir cómo resolver el conflicto seleccionando una de las versiones proporcionadas o fusionando los datos de las dos versiones guardadas del juego.

Para detectar y resolver conflictos de juegos guardados, haz lo siguiente:

  1. Llama a SnapshotsClient.open(). El resultado de la tarea contiene una clase SnapshotsClient.DataOrConflict.
  2. Llama al método SnapshotsClient.DataOrConflict.isConflict(). Si el resultado es verdadero, tienes un conflicto para resolver.
  3. Llama a SnapshotsClient.DataOrConflict.getConflict() para recuperar una instancia de SnaphotsClient.snapshotConflict.
  4. Llama a SnapshotsClient.SnapshotConflict.getConflictId() para recuperar el ID de conflicto que identifica de forma única el conflicto detectado. Tu juego necesita este valor para enviar una solicitud de resolución de conflicto más adelante.
  5. Llama a SnapshotsClient.SnapshotConflict.getConflictingSnapshot() para obtener la versión local.
  6. Llama a SnapshotsClient.SnapshotConflict.getSnapshot() para obtener la versión del servidor.
  7. Para resolver el conflicto del juego guardado, selecciona una versión que quieras guardar en el servidor como la versión final y pásala al método SnapshotsClient.resolveConflict().

En el siguiente fragmento, se muestra un ejemplo de cómo el juego podría controlar un conflicto de juego guardado seleccionando el que se modificó recientemente como la versión final para guardar:


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");
              }
            }
          });
}

Modificación de los juegos guardados para la resolución de conflictos

Si deseas combinar datos de varios juegos guardados o modificar un Snapshot existente para guardarlo en el servidor como la versión final resuelta, sigue estos pasos:

  1. Llama al SnapshotsClient.open() .
  2. Llama a SnapshotsClient.SnapshotConflict.getResolutionSnapshotsContent() para obtener un nuevo objeto SnapshotContents.
  3. Combina los datos de SnapshotsClient.SnapshotConflict.getConflictingSnapshot() y SnapshotsClient.SnapshotConflict.getSnapshot() en el objeto SnapshotContents del paso anterior.
  4. De manera opcional, crea una instancia SnapshotMetadataChange si se realizan cambios en los campos de metadatos.
  5. Llama a SnapshotsClient.resolveConflict(). En tu llamada de método, pasa SnapshotsClient.SnapshotConflict.getConflictId() como el primer argumento, y los objetos SnapshotMetadataChange y SnapshotContents que modificaste antes como el segundo y el tercer argumento, respectivamente.
  6. Si la llamada a SnapshotsClient.resolveConflict() se realiza de forma correcta, la API almacena el objeto Snapshot en el servidor y, luego, intenta abrir el objeto Snapshot en tu dispositivo local.