Prise en charge des jeux enregistrés dans les jeux Android

Ce guide explique comment implémenter des jeux enregistrés à l'aide de l'API Snapshots fournie par les services de jeux Google Play. Les API sont disponibles dans les packages com.google.android.gms.games.snapshot et com.google.android.gms.games.

Avant de commencer

Si ce n'est pas déjà fait, n'hésitez pas à revoir les concepts de jeux de sauvegarde.

Obtenir le client Snapshots

Pour commencer à utiliser l'API Snapshots, votre jeu doit d'abord se procurer un objet SnapshotsClient. Pour ce faire, appelez la méthode Games.getSnapshotsClient() et transmettez l'activité et le GoogleSignInAccount pour le lecteur actuel. Pour savoir comment récupérer les informations du compte du joueur, consultez Se connecter dans les jeux Android.

Spécifier le champ d'application de Drive

L'API Snapshots s'appuie sur l'API Google Drive pour le stockage des jeux enregistrés. Pour accéder à l'API Drive, votre application doit spécifier le champ d'application Drive.SCOPE_APPFOLDER lors de la création du client Google Sign-In.

L'exemple suivant contient la procédure à suivre dans la méthode onResume() pour votre activité de connexion :

private GoogleSignInClient mGoogleSignInClient;

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

Afficher les jeux enregistrés

Vous pouvez intégrer l'API Snapshots partout où votre jeu offre aux joueurs la possibilité d'enregistrer ou de restaurer leur progression. Votre jeu peut afficher une option de ce type à certains points d'enregistrement/de restauration, ou permettre aux joueurs d'enregistrer ou de restaurer leur progression à tout moment.

Une fois l'option d'enregistrement ou de restauration sélectionnée, le jeu peut également afficher un écran qui invite le joueur à saisir les informations d'un nouveau jeu enregistré ou à sélectionner un jeu existant à restaurer.

Pour simplifier le développement, l'API Snapshots fournit une interface utilisateur (UI) de sélection de jeux enregistrés par défaut prête à l'emploi L'UI de sélection de jeux enregistrés permet aux joueurs de créer un jeu enregistré, de consulter les détails des jeux enregistrés existants et de charger de précédents jeux enregistrés.

Pour lancer l'UI Jeux enregistrés par défaut :

  1. Appelez SnapshotsClient.getSelectSnapshotIntent() pour obtenir un élément Intent permettant de lancer l'UI de sélection de jeux enregistrés par défaut.
  2. Appelez startActivityForResult() et transmettez cet élément Intent. Si l'appel aboutit, le jeu affiche l'UI de sélection de jeux enregistrés, ainsi que les options que vous avez spécifiées.

Voici un exemple de lancement de l'UI de sélection de jeux enregistrés par défaut :

private static final int RC_SAVED_GAMES = 9009;

private void showSavedGamesUI() {
  SnapshotsClient snapshotsClient =
      Games.getSnapshotsClient(this, GoogleSignIn.getLastSignedInAccount(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 le joueur choisit de créer un jeu enregistré ou de charger un jeu enregistré existant, l'UI envoie une requête aux services de jeux Google Play. Si la requête aboutit, les services de jeux Google Play renvoient les informations permettant de créer ou de restaurer le jeu enregistré via le rappel onActivityResult(). Votre jeu peut ignorer ce rappel pour vérifier si des erreurs se sont produites lors de la requête.

L'extrait de code suivant fournit un exemple d'implémentation 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
      // ...
    }
  }
}

Écrire des jeux enregistrés

Pour stocker du contenu dans un jeu enregistré :

  1. Ouvrez un instantané de manière asynchrone via SnapshotsClient.open(). Ensuite, récupérez l'objet Snapshot à partir du résultat de la tâche en appelant SnapshotsClient.DataOrConflict.getData().
  2. Récupérez une instance SnapshotContents via SnapshotsClient.SnapshotConflict.
  3. Appelez SnapshotContents.writeBytes() pour stocker les données du joueur au format octet.
  4. Une fois toutes vos modifications écrites, appelez SnapshotsClient.commitAndClose() pour les envoyer aux serveurs de Google. Dans l'appel de méthode, votre jeu peut fournir des informations supplémentaires afin d'indiquer aux services de jeux Google Play comment présenter le jeu enregistré aux joueurs. Ces informations sont représentées dans un objet SnapshotMetaDataChange, que votre jeu crée à l'aide de SnapshotMetadataChange.Builder.

L'extrait de code suivant montre comment votre jeu peut apporter des modifications à un jeu enregistré :

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 =
      Games.getSnapshotsClient(this, GoogleSignIn.getLastSignedInAccount(this));

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

Si l'appareil du joueur n'est pas connecté à un réseau au moment où votre application appelle SnapshotsClient.commitAndClose(), les services de jeux Google Play stockent les données du jeu enregistré localement sur l'appareil. Lors de la reconnexion de l'appareil, les services de jeux Google Play synchronisent les modifications du jeu enregistré qui ont été mises en cache localement avec les serveurs de Google.

Charger des jeux enregistrés

Pour récupérer les jeux enregistrés du joueur actuellement connecté :

  1. Ouvrez un instantané de manière asynchrone via SnapshotsClient.open(). Ensuite, récupérez l'objet Snapshot à partir du résultat de la tâche en appelant SnapshotsClient.DataOrConflict.getData(). Votre jeu peut également récupérer un instantané spécifique via l'UI de sélection de jeux enregistrés, comme décrit dans Afficher les jeux enregistrés.
  2. Récupérez l'instance SnapshotContents via SnapshotsClient.SnapshotConflict.
  3. Appelez SnapshotContents.readFully() pour lire le contenu de l'instantané.

L'extrait de code suivant montre comment charger un jeu enregistré spécifique :

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

  // Get the SnapshotsClient from the signed in account.
  SnapshotsClient snapshotsClient =
      Games.getSnapshotsClient(this, GoogleSignIn.getLastSignedInAccount(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.
          // ...
        }
      });
}

Gérer les conflits liés aux jeux enregistrés

Lorsque vous utilisez l'API Snapshots dans votre jeu, il peut arriver que plusieurs appareils exécutent des opérations de lecture et d'écriture dans le même jeu enregistré. En cas de perte de connexion temporaire, puis de reconnexion d'un appareil, des conflits de données peuvent se produire, car le jeu enregistré stocké sur l'appareil local du joueur n'est plus synchronisé avec la version distante stockée sur les serveurs de Google.

L'API Snapshots fournit un mécanisme de résolution des conflits qui présente les deux jeux enregistrés en conflit au moment de la lecture et vous permet d'implémenter une stratégie de résolution adaptée à votre jeu.

Lorsque les services de jeux Google Play détectent un conflit de données, la méthode SnapshotsClient.DataOrConflict.isConflict() renvoie la valeur true. Dans ce cas, la classe SnapshotsClient.SnapshotConflict fournit deux versions du jeu enregistré:

  • Version du serveur: version la plus récente connue des services Google Play Jeux comme étant exacte pour l'appareil du joueur.
  • Local version (Version locale) : version modifiée détectée sur l'un des appareils du joueur et qui comporte du contenu ou des métadonnées en conflit. Elle peut être différente de la version que vous avez essayé d'enregistrer.

Votre jeu doit déterminer comment résoudre le conflit en choisissant l'une des versions fournies ou en fusionnant les données des deux versions enregistrées.

Pour détecter et résoudre les conflits liés aux jeux enregistrés :

  1. Appelez SnapshotsClient.open(). Le résultat de la tâche contient une classe SnapshotsClient.DataOrConflict.
  2. Appelez la méthode SnapshotsClient.DataOrConflict.isConflict(). Si le résultat est "true", cela signifie que vous avez un conflit à résoudre.
  3. Appelez SnapshotsClient.DataOrConflict.getConflict() pour récupérer une instance de SnaphotsClient.snapshotConflict.
  4. Appelez SnapshotsClient.SnapshotConflict.getConflictId() pour récupérer l'identifiant unique du conflit détecté. Votre jeu a besoin de cette valeur pour envoyer ultérieurement une requête de résolution de conflit.
  5. Appelez SnapshotsClient.SnapshotConflict.getConflictingSnapshot() pour obtenir la version locale.
  6. Appelez SnapshotsClient.SnapshotConflict.getSnapshot() pour obtenir la version du serveur.
  7. Pour résoudre le conflit entre jeux enregistrés, sélectionnez la version que vous souhaitez enregistrer sur le serveur en tant que version finale, puis transmettez-la à la méthode SnapshotsClient.resolveConflict().

L'extrait de code suivant montre comment votre jeu peut gérer un conflit entre jeux enregistrés en sélectionnant celui dont les modifications sont les plus récentes comme version finale à enregistrer :

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 Games.getSnapshotsClient(theActivity, GoogleSignIn.getLastSignedInAccount(this))
      .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");
              }
            }
          });
}

Modifier des jeux enregistrés pour résoudre les conflits

Si vous souhaitez fusionner les données de plusieurs jeux enregistrés ou modifier un objet Snapshot existant pour l'enregistrer sur le serveur en tant que version finale résolue, procédez comme suit:

  1. Appelez SnapshotsClient.open() .
  2. Appelez SnapshotsClient.SnapshotConflict.getResolutionSnapshotsContent() pour obtenir un nouvel objet SnapshotContents.
  3. Fusionnez les données de SnapshotsClient.SnapshotConflict.getConflictingSnapshot() et SnapshotsClient.SnapshotConflict.getSnapshot() dans l'objet SnapshotContents de l'étape précédente.
  4. Vous pouvez également créer une instance de SnapshotMetadataChange si des modifications sont apportées aux champs de métadonnées.
  5. Appelez SnapshotsClient.resolveConflict(). Dans votre appel de méthode, transmettez SnapshotsClient.SnapshotConflict.getConflictId() comme premier argument, et les objets SnapshotMetadataChange et SnapshotContents que vous avez modifiés précédemment comme deuxième et troisième arguments.
  6. Si l'appel SnapshotsClient.resolveConflict() aboutit, l'API stocke l'objet Snapshot sur le serveur et tente d'ouvrir l'objet Snapshot sur votre appareil local.