Support für gespeicherte Spiele in Android-Spielen

In diesem Leitfaden wird erläutert, wie Sie gespeicherte Spiele mit der Snapshots API implementieren, die von den Google Play-Spieldiensten bereitgestellt wird. Die APIs finden Sie in den Paketen com.google.android.gms.games.snapshot und com.google.android.gms.games.

Hinweis

Falls Sie es noch nicht getan haben, kann es hilfreich sein, sich die Spielekonzepte für gespeicherte Spiele durchzulesen.

Snapshot-Client abrufen

Damit Sie die Snapshots API verwenden können, muss Ihr Spiel zuerst ein SnapshotsClient-Objekt abrufen. Rufen Sie dazu die Methode Games.getSnapshotsClient() auf und übergeben Sie die Aktivität.

Drive-Bereich angeben

Die Snapshots API nutzt die Google Drive API zum Speichern gespeicherter Spiele. Ihre Anwendung muss für den Zugriff auf die Drive API beim Erstellen des Google-Anmeldeclients den Bereich Drive.SCOPE_APPFOLDER angeben.

Hier ein Beispiel dafür, wie dies in der Methode onResume() für deine Anmeldeaktivität funktioniert:


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

Gespeicherte Spiele werden angezeigt

Sie können die Snapshots API überall dort einbinden, wo ein Spiel für Spieler verfügbar ist und sie ihren Fortschritt speichern oder wiederherstellen können. Ihr Spiel könnte eine solche Option an bestimmten Speicherpunkten zum Wiederherstellen oder Wiederherstellen anzeigen oder den Spielern ermöglichen, den Fortschritt jederzeit zu speichern oder wiederherzustellen.

Nachdem Spieler die Option „Speichern/Wiederherstellen“ in Ihrem Spiel ausgewählt haben, kann Ihr Spiel optional einen Bildschirm aufrufen, auf dem Spieler aufgefordert werden, Informationen für ein neues gespeichertes Spiel einzugeben oder ein vorhandenes gespeichertes Spiel wiederherzustellen.

Um die Entwicklung zu vereinfachen, bietet die Snapshot API eine standardmäßige gespeicherte Benutzeroberfläche für Spiele, die Sie sofort verwenden können. Über die UI zur Auswahl von gespeicherten Spielen können Spieler ein neues gespeichertes Spiel erstellen, Details zu vorhandenen gespeicherten Spielen ansehen und zuvor gespeicherte Spiele laden.

So starten Sie die standardmäßige Benutzeroberfläche für gespeicherte Spiele:

  1. Rufen Sie SnapshotsClient.getSelectSnapshotIntent() auf, um einen Intent zum Starten der Standard-UI für die Auswahl gespeicherter Spiele zu erhalten.
  2. Rufen Sie startActivityForResult() auf und übergeben Sie Intent. Wenn der Aufruf erfolgreich ist, zeigt das Spiel die gespeicherte UI zur Spieleauswahl sowie die von Ihnen angegebenen Optionen an.

Hier ein Beispiel für die Einführung der standardmäßigen UI für die Auswahl gespeicherter Spiele:

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

Wenn der Spieler ein neues gespeichertes Spiel erstellt oder ein gespeichertes Spiel lädt, sendet die UI eine Anfrage an die Google Play-Spieldienste. Wenn die Anfrage erfolgreich ist, geben die Google Play-Spieldienste Informationen zum Erstellen oder Wiederherstellen des gespeicherten Spiels über den onActivityResult()-Callback zurück. Dein Spiel kann diesen Callback überschreiben, um zu prüfen, ob während der Anfrage Fehler aufgetreten sind.

Das folgende Code-Snippet zeigt eine Beispielimplementierung von 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
      // ...
    }
  }
}

Gespeicherte Spiele schreiben

So speichern Sie Inhalte in einem gespeicherten Spiel:

  1. Öffnen Sie einen Snapshot über SnapshotsClient.open() asynchron. Rufen Sie dann das Objekt Snapshot aus dem Ergebnis der Aufgabe ab. Rufen Sie dazu SnapshotsClient.DataOrConflict.getData() auf.
  2. Rufen Sie eine SnapshotContents-Instanz über SnapshotsClient.SnapshotConflict ab.
  3. Rufe SnapshotContents.writeBytes() auf, um die Spielerdaten im Byteformat zu speichern.
  4. Nachdem alle Änderungen geschrieben sind, rufen Sie SnapshotsClient.commitAndClose() auf, um Ihre Änderungen an die Google-Server zu senden. Im Methodenaufruf kann Ihr Spiel optional zusätzliche Informationen enthalten, um den Google Play-Spieldiensten mitzuteilen, wie das gespeicherte Spiel den Spielern präsentiert werden soll. Diese Informationen werden in einem SnapshotMetaDataChange-Objekt dargestellt, das Ihr Spiel mit SnapshotMetadataChange.Builder erstellt.

Das folgende Snippet zeigt, wie in einem Spiel Änderungen an einem gespeicherten Spiel angewendet werden:

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

Wenn das Gerät des Spielers nicht mit einem Netzwerk verbunden ist, wenn deine App SnapshotsClient.commitAndClose() aufruft, speichern die Google Play-Spieldienste die gespeicherten Spieldaten lokal auf dem Gerät. Wenn das Gerät wieder verbunden ist, synchronisieren die Google Play-Spieldienste die lokal im Cache gespeicherten Spieländerungen mit den Google-Servern.

Gespeicherte Spiele werden geladen

So rufen Sie gespeicherte Spiele für den aktuell angemeldeten Spieler ab:

  1. Öffnen Sie einen Snapshot über SnapshotsClient.open() asynchron. Rufen Sie dann das Objekt Snapshot aus dem Ergebnis der Aufgabe ab. Rufen Sie dazu SnapshotsClient.DataOrConflict.getData() auf. Alternativ kann Ihr Spiel auch einen bestimmten Snapshot über die Benutzeroberfläche für die Auswahl gespeicherter Spiele abrufen, wie unter Gespeicherte Spiele anzeigen beschrieben.
  2. Rufen Sie die Instanz SnapshotContents über SnapshotsClient.SnapshotConflict ab.
  3. Rufen Sie SnapshotContents.readFully() auf, um den Inhalt des Snapshots zu lesen.

Das folgende Snippet zeigt, wie Sie ein bestimmtes gespeichertes Spiel laden können:

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

Umgang mit gespeicherten Spielkonflikten

Wenn Sie die Snapshot API in Ihrem Spiel verwenden, können mehrere Geräte Lese- und Schreibvorgänge für dasselbe gespeicherte Spiel ausführen. Falls ein Gerät vorübergehend die Netzwerkverbindung verliert und sich später wieder verbindet, kann es zu Datenkonflikten kommen. Das gespeicherte Spiel auf dem lokalen Gerät des Spielers ist dann nicht mehr mit der Remote-Version kompatibel, die auf den Servern von Google gespeichert ist.

Die Snapshots API bietet einen Mechanismus zur Konfliktlösung, bei dem beide Gruppen von in Konflikt stehenden gespeicherten Spielen zum Lesezeitpunkt aufgeführt werden. Außerdem können Sie eine geeignete Lösung für Ihr Spiel implementieren.

Wenn die Google Play-Spieldienste einen Datenkonflikt erkennen, gibt die Methode SnapshotsClient.DataOrConflict.isConflict() den Wert true zurück. In diesem Fall bietet die Klasse SnapshotsClient.SnapshotConflict zwei Versionen des gespeicherten Spiels:

  • Serverversion: die aktuelle Version, die Google Play-Spieldienste für die Verwendung auf dem Gerät des Spielers aktuell kennen
  • Lokale Version: Eine geänderte Version auf einem der Geräte des Players, die Inhalte oder Metadaten enthält, die miteinander in Konflikt stehen. Sie muss nicht mit der Version übereinstimmen, die Sie speichern möchten.

Ihr Spiel muss entscheiden, wie der Konflikt gelöst werden soll, indem eine der angegebenen Versionen ausgewählt oder die Daten der beiden gespeicherten Spieleversionen zusammengeführt werden.

So können Sie gespeicherte Spielkonflikte erkennen und beheben:

  1. Rufen Sie SnapshotsClient.open() auf. Das Ergebnis der Aufgabe enthält die Klasse SnapshotsClient.DataOrConflict.
  2. Rufen Sie die Methode SnapshotsClient.DataOrConflict.isConflict() auf. Wenn das Ergebnis wahr ist, müssen Sie einen Konflikt lösen.
  3. Rufen Sie SnapshotsClient.DataOrConflict.getConflict() auf, um eine SnaphotsClient.snapshotConflict-Instanz abzurufen.
  4. Rufen Sie SnapshotsClient.SnapshotConflict.getConflictId() auf, um die Konflikt-ID abzurufen, die den erkannten Konflikt eindeutig identifiziert. Ihr Spiel benötigt diesen Wert, um später eine Anfrage zur Konfliktlösung senden zu können.
  5. Rufen Sie SnapshotsClient.SnapshotConflict.getConflictingSnapshot() auf, um die lokale Version abzurufen.
  6. Rufen Sie SnapshotsClient.SnapshotConflict.getSnapshot() auf, um die Serverversion abzurufen.
  7. Zum Lösen des gespeicherten Spielkonflikts wählen Sie eine Version aus, die Sie als endgültige Version auf dem Server speichern möchten, und übergeben sie an die Methode SnapshotsClient.resolveConflict().

Das folgende Snippet zeigt und zeigt, wie mit einem gespeicherten Konflikt ein Spiel möglicherweise gehandhabt wird, indem das zuletzt gespeicherte Spiel als finale Version zum Speichern ausgewählt wird:


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

Gespeicherte Spiele zur Konfliktlösung ändern

Wenn Sie Daten aus mehreren gespeicherten Spielen zusammenführen oder eine vorhandene Snapshot ändern möchten, um sie als aufgelöste endgültige Version auf dem Server zu speichern, gehen Sie so vor:

  1. Rufen Sie SnapshotsClient.open() auf.
  2. Rufen Sie SnapshotsClient.SnapshotConflict.getResolutionSnapshotsContent() auf, um ein neues SnapshotContents-Objekt abzurufen.
  3. Führen Sie die Daten aus SnapshotsClient.SnapshotConflict.getConflictingSnapshot() und SnapshotsClient.SnapshotConflict.getSnapshot() mit dem Objekt SnapshotContents aus dem vorherigen Schritt zusammen.
  4. Erstellen Sie optional eine SnapshotMetadataChange-Instanz, wenn Änderungen an den Metadatenfeldern vorgenommen wurden.
  5. Rufen Sie SnapshotsClient.resolveConflict() auf. Übergeben Sie in Ihrem Methodenaufruf SnapshotsClient.SnapshotConflict.getConflictId() als erstes Argument und die SnapshotMetadataChange- und SnapshotContents-Objekte, die Sie zuvor als zweites bzw. drittes Argument geändert haben.
  6. Wenn der SnapshotsClient.resolveConflict()-Aufruf erfolgreich war, speichert die API das Snapshot-Objekt auf dem Server und versucht, das Snapshot-Objekt auf Ihrem lokalen Gerät zu öffnen.