Gespeicherte Spiele für Android-Spiele

In dieser Anleitung wird beschrieben, wie du gespeicherte Spiele mit der Snapshots API der Google Play-Spieldienste implementierst. Die APIs sind in den Paketen com.google.android.gms.games.snapshot und com.google.android.gms.games enthalten.

Vorbereitung

Informationen zu der Funktion finden Sie in der Übersicht zu gespeicherten Spielen.

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.

Laufwerkbereich angeben

Die Snapshots API nutzt die Google Drive API für die Speicherung gespeicherter Spiele. Für den Zugriff auf die Drive API muss in der Anwendung beim Erstellen des Google Log-in-Clients der Bereich Drive.SCOPE_APPFOLDER angegeben werden.

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


@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 anzeigen

Sie können die Snapshots API überall dort integrieren, wo Ihr Spiel Spielern die Möglichkeit bietet, ihren Fortschritt zu speichern oder wiederherzustellen. Ihr Spiel kann an bestimmten Speicher-/Wiederherstellungspunkten eine solche Option anzeigen oder den Spielern die Möglichkeit bieten, den Fortschritt jederzeit zu speichern oder wiederherzustellen.

Nachdem Nutzer in Ihrem Spiel die Option zum Speichern/Wiederherstellen ausgewählt haben, kann Ihr Spiel optional einen Bildschirm aufrufen, auf dem sie aufgefordert werden, Informationen für ein neues gespeichertes Spiel einzugeben oder ein vorhandenes gespeichertes Spiel für die Wiederherstellung auszuwählen.

Die Snapshots API bietet eine standardmäßige Benutzeroberfläche zur Auswahl gespeicherter Spiele, die Sie sofort verwenden können, um die Entwicklung zu vereinfachen. Über die Benutzeroberfläche zur Auswahl gespeicherter Spiele können Spieler ein neues gespeichertes Spiel erstellen, Details zu vorhandenen gespeicherten Spielen ansehen und frühere gespeicherte Spiele laden.

So öffnen Sie die Standard-UI für gespeicherte Spiele:

  1. Rufen Sie SnapshotsClient.getSelectSnapshotIntent() auf, um ein Intent-Element zum Starten der Standard-UI zur Auswahl gespeicherter Spiele zu erhalten.
  2. Rufen Sie startActivityForResult() auf und übergeben Sie diese Intent. Wenn der Aufruf erfolgreich ist, zeigt das Spiel die Benutzeroberfläche für die Auswahl des gespeicherten Spiels zusammen mit den von Ihnen festgelegten Optionen an.

Hier ein Beispiel, wie die Standard-Benutzeroberfläche zur Auswahl gespeicherter Spiele geöffnet wird:

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 sich der Spieler dafür entscheidet, ein neues gespeichertes Spiel zu erstellen oder ein vorhandenes gespeichertes Spiel zu laden, sendet die Benutzeroberfläche eine Anfrage an die Play-Spieldienste. Wenn die Anfrage erfolgreich ist, geben die 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 bei 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 asynchron über SnapshotsClient.open().

  2. Rufen Sie das Objekt Snapshot aus dem Ergebnis der Aufgabe ab, indem Sie SnapshotsClient.DataOrConflict.getData() aufrufen.

  3. Rufen Sie mit SnapshotsClient.SnapshotConflict eine SnapshotContents-Instanz ab.

  4. Rufe SnapshotContents.writeBytes() auf, um die Daten des Players im Byte-Format zu speichern.

  5. Sobald alle Änderungen geschrieben sind, rufen Sie SnapshotsClient.commitAndClose() auf, um sie an die Google-Server zu senden. Im Methodenaufruf kann Ihr Spiel optional zusätzliche Informationen bereitstellen, um den Play-Spieldiensten mitzuteilen, wie dieses gespeicherte Spiel den Spielern präsentiert werden soll. Diese Informationen werden in einem SnapshotMetaDataChange-Objekt dargestellt, das dein Spiel mit SnapshotMetadataChange.Builder erstellt.

Das folgende Snippet zeigt, wie Ihr Spiel Änderungen an einem gespeicherten Spiel durchführen könnte:

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 beim Aufrufen von SnapshotsClient.commitAndClose() durch Ihre App nicht mit einem Netzwerk verbunden ist, speichern die Play-Spieldienste die gespeicherten Spieldaten lokal auf dem Gerät. Sobald das Gerät wieder verbunden ist, synchronisieren die Play-Spieldienste die lokal im Cache gespeicherten Spieländerungen mit den Google-Servern.

Gespeicherte Spiele laden

So rufen Sie gespeicherte Spiele des derzeit angemeldeten Spielers ab:

  1. Öffnen Sie einen Snapshot mit SnapshotsClient.open() asynchron.

  2. Rufen Sie das Objekt Snapshot aus dem Ergebnis der Aufgabe ab, indem Sie SnapshotsClient.DataOrConflict.getData() aufrufen. 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.

  3. Rufen Sie die SnapshotContents-Instanz mit SnapshotsClient.SnapshotConflict ab.

  4. 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 Konflikten in gespeicherten Spielen

Wenn Sie in Ihrem Spiel die Snapshots API verwenden, können mehrere Geräte Lese- und Schreibvorgänge im selben gespeicherten Spiel ausführen. Falls ein Gerät vorübergehend seine Netzwerkverbindung verliert und später wieder eine Verbindung herstellt, kann dies zu Datenkonflikten führen, bei denen das auf dem lokalen Gerät eines Spielers gespeicherte Spiel nicht mehr mit der Remote-Version auf den Servern von Google synchronisiert ist.

Die Snapshots API bietet einen Mechanismus zur Konfliktlösung, der beide Gruppen von in Konflikt stehenden gespeicherten Spielen zur Lesezeit anzeigt und Sie eine Lösungsstrategie implementieren kann, die für Ihr Spiel geeignet ist.

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

  • Serverversion: Die aktuellste Version, die den Play-Spieldiensten für das Gerät des Spielers aktuell bekannt ist.

  • Lokale Version: Eine modifizierte Version, die auf einem der Geräte des Players erkannt wurde und in Konflikt stehende Inhalte oder Metadaten enthält. Dies ist möglicherweise nicht mit der Version identisch, die Sie speichern wollten.

Ihr Spiel muss entscheiden, wie der Konflikt behoben werden soll, indem eine der bereitgestellten Versionen ausgewählt oder die Daten der beiden gespeicherten Spielversionen zusammengeführt werden.

So erkennen und lösen Sie Konflikte in gespeicherten Spielen:

  1. Rufen Sie SnapshotsClient.open() auf. Das Ergebnis der Aufgabe enthält eine SnapshotsClient.DataOrConflict-Klasse.

  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. Dieser Wert wird benötigt, um später eine Anfrage zur Konfliktlösung zu senden.

  5. Rufen Sie SnapshotsClient.SnapshotConflict.getConflictingSnapshot() auf, um die lokale Version zu erhalten.

  6. Rufen Sie SnapshotsClient.SnapshotConflict.getSnapshot() auf, um die Serverversion zu erhalten.

  7. Wählen Sie zum Beheben des Konflikts im gespeicherten Spiel eine Version aus, die Sie als endgültige Version auf dem Server speichern möchten, und übergeben Sie sie an die Methode SnapshotsClient.resolveConflict().

Das folgende Snippet zeigt ein Beispiel dafür, wie Ihr Spiel einen Konflikt mit einem gespeicherten Spiel handhaben könnte, indem das zuletzt geänderte gespeicherte Spiel als endgültige Version ausgewählt wird, die gespeichert werden soll:


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 ändern

Wenn du Daten aus mehreren gespeicherten Spielen zusammenführen oder ein vorhandenes Snapshot ändern möchtest, um es als aufgelöste endgültige Version auf dem Server zu speichern, gehe so vor:

  1. Rufen Sie SnapshotsClient.open() auf.

  2. Rufen Sie SnapshotsClient.SnapshotConflict.getResolutionSnapshotsContent() auf, um ein neues SnapshotContents-Objekt zu erhalten.

  3. Führen Sie die Daten aus SnapshotsClient.SnapshotConflict.getConflictingSnapshot() und SnapshotsClient.SnapshotConflict.getSnapshot() im SnapshotContents-Objekt aus dem vorherigen Schritt zusammen.

  4. Erstellen Sie optional eine SnapshotMetadataChange-Instanz, falls Änderungen an den Metadatenfeldern vorgenommen werden.

  5. Rufen Sie SnapshotsClient.resolveConflict() auf. Übergeben Sie im Methodenaufruf SnapshotsClient.SnapshotConflict.getConflictId() als erstes Argument und die Objekte SnapshotMetadataChange und SnapshotContents, die Sie zuvor geändert haben, jeweils als zweites bzw. drittes Argument.

  6. Wenn der SnapshotsClient.resolveConflict()-Aufruf erfolgreich ist, speichert die API das Snapshot-Objekt auf dem Server und versucht, das Snapshot-Objekt auf Ihrem lokalen Gerät zu öffnen.