Android 遊戲中的遊戲進度存檔

本指南說明如何使用 Google Play 遊戲服務提供的快照 API 實作已儲存的遊戲。您可以在 com.google.android.gms.games.snapshotcom.google.android.gms.games 套件中取得此 API。

事前準備

如果您尚未執行,請參閱遊戲進度存檔概念

取得快照用戶端

如要開始使用快照 API,遊戲必須先取得 SnapshotsClient 物件。要取得此物件,您可以呼叫 Games.getSnapshotsClient() 方法,然後在活動內傳遞。

指定雲端硬碟範圍

快照 API 需透過 Google Drive API 儲存遊戲儲存空間。如要存取 Drive API,您的應用程式必須在建立 Google 登入用戶端時指定 Drive.SCOPE_APPFOLDER 範圍。

以下範例說明如何在登入活動的 onResume() 方法中執行此操作:


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

目前顯示的是已儲存的遊戲

您可以在遊戲的任何位置整合加入快照 API,讓玩家可以選擇儲存或還原進度。您的遊戲可能會在指定的儲存/還原點顯示此類選項,也可以允許玩家隨時儲存或還原進度。

玩家選取遊戲中的儲存/還原選項後,遊戲會另外顯示一個畫面,提示玩家輸入新遊戲進度資訊,或者選取要還原的現有遊戲進度存檔。

為簡化開發作業,快照 API 提供預設儲存的遊戲選取使用者介面 (UI),可立即使用。遊戲進度存檔選項 UI 可讓玩家建立新的遊戲進度存檔、查看現有遊戲進度存檔,以及載入先前儲存的遊戲進度存檔。

如要啟動預設的遊戲進度存檔 UI:

  1. 呼叫 SnapshotsClient.getSelectSnapshotIntent() 以取得 Intent,用於啟動預設儲存的遊戲選擇 UI。
  2. 呼叫 startActivityForResult() 並傳入 Intent。如果呼叫成功,遊戲會顯示遊戲進度存檔選擇 UI 以及指定的選項。

以下範例說明如何啟動預設的遊戲進度存檔選擇 UI:

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

如果玩家選擇建立新的已儲存的遊戲,或載入現有的已儲存的遊戲,UI 就會傳送要求給 Google Play 遊戲服務。如果要求成功,Google Play 遊戲服務會傳回資訊,以便透過 onActivityResult() 回呼建立或還原已儲存的遊戲。遊戲可能會覆寫此回呼,以確認要求期間是否發生任何錯誤。

下列程式碼片段顯示 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
      // ...
    }
  }
}

撰寫遊戲進度通知

在遊戲進度存檔中儲存內容:

  1. 透過 SnapshotsClient.open() 以非同步方式開啟快照。接著,請呼叫 SnapshotsClient.DataOrConflict.getData(),從工作結果中擷取 Snapshot 物件。
  2. 透過 SnapshotsClient.SnapshotConflict 擷取 SnapshotContents 執行個體。
  3. 呼叫 SnapshotContents.writeBytes() 即可以位元組格式儲存玩家的資料。
  4. 寫入所有變更之後,請呼叫 SnapshotsClient.commitAndClose() 將變更內容傳送至 Google 的伺服器。在方法呼叫中,您的遊戲可以選擇提供其他資訊,讓 Google Play 遊戲服務如何向玩家呈現這個已儲存遊戲。此資訊表示在遊戲使用 SnapshotMetadataChange.Builder 建立的 SnapshotMetaDataChange 物件中。

下列程式碼片段顯示遊戲如何對遊戲進度存檔做出變更:

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

如果您的應用程式呼叫 SnapshotsClient.commitAndClose(),但玩家的裝置未連線至網路,Google Play 遊戲服務會將已儲存的遊戲資料儲存在裝置上。裝置重新連線後,Google Play 遊戲服務會將本機快取儲存的遊戲變更同步到 Google 的伺服器。

正在載入已儲存的遊戲

擷取目前登入玩家的遊戲進度存檔:

  1. 透過 SnapshotsClient.open() 以非同步方式開啟快照。接著,請呼叫 SnapshotsClient.DataOrConflict.getData(),從工作結果中擷取 Snapshot 物件。或者,您的遊戲也可以透過已儲存的遊戲選取 UI 擷取特定快照,如顯示已儲存的遊戲一節所述。
  2. 透過 SnapshotsClient.SnapshotConflict 擷取 SnapshotContents 執行個體。
  3. 呼叫 SnapshotContents.readFully() 以讀取快照內容。

下列程式碼片段說明如何載入特定的遊戲進度存檔:

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

處理遊戲進度衝突

在遊戲中使用快照 API 時,多部裝置或許可以在同一個遊戲進度存檔中執行讀取和寫入作業。如果裝置暫時中斷網路連線,稍後又重新連線,就可能會造成資料衝突,進而導致玩家在本機裝置中的遊戲進度存檔與 Google 伺服器中儲存的遠端版本不同步。

快照 API 提供衝突的解決機制,可在讀取時同時顯示兩組互相衝突的遊戲進度存檔,讓您可以依照適合遊戲的策略方式進行解決。

當 Google Play 遊戲服務偵測到資料衝突時,SnapshotsClient.DataOrConflict.isConflict() 方法會傳回 true 的值。在這個事件中,SnapshotsClient.SnapshotConflict 類別會提供兩個已儲存遊戲版本的版本:

  • 伺服器版本:Google Play 遊戲服務已知且符合玩家裝置的最新版本;以及
  • 本機版本:在玩家的裝置上偵測到修改版本,當中含有衝突的內容或中繼資料。可能與您嘗試儲存的版本不同。

您的遊戲必須決定如何解決衝突問題:選擇其中一個版本,或合併兩個遊戲進度存檔版本的資料。

偵測並解決遊戲進度存檔衝突問題:

  1. 呼叫 SnapshotsClient.open()。工作結果包含 SnapshotsClient.DataOrConflict 類別。
  2. 呼叫 SnapshotsClient.DataOrConflict.isConflict()方法。如果結果為 true,您就會得到衝突。
  3. 呼叫 SnapshotsClient.DataOrConflict.getConflict() 以擷取 SnaphotsClient.snapshotConflict 執行個體。
  4. 呼叫 SnapshotsClient.SnapshotConflict.getConflictId() 以擷取可識別偵測到衝突的專屬衝突 ID。您的遊戲需要這個值來稍後傳送衝突解決要求。
  5. 呼叫 SnapshotsClient.SnapshotConflict.getConflictingSnapshot() 即可取得本機版本。
  6. 呼叫 SnapshotsClient.SnapshotConflict.getSnapshot() 以取得伺服器版本。
  7. 如要解決已儲存的遊戲衝突,請選取您要儲存至最終版本的版本做為最終版本,然後將該版本傳送至 SnapshotsClient.resolveConflict() 方法。

以下程式碼片段範例說明遊戲在處理最終版本時,選擇最近修改過的遊戲進度存檔,以處理遊戲進度存檔衝突情況:


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

解決遊戲問題以解決衝突問題

如果您想要合併多個已儲存遊戲的資料,或是修改現有的 Snapshot 以作為已解析的最終版本,並儲存至伺服器,請按照下列步驟操作:

  1. 呼叫 SnapshotsClient.open()
  2. 呼叫 SnapshotsClient.SnapshotConflict.getResolutionSnapshotsContent() 以取得新的 SnapshotContents 物件。
  3. SnapshotsClient.SnapshotConflict.getConflictingSnapshot()SnapshotsClient.SnapshotConflict.getSnapshot() 的資料合併到上一步中的 SnapshotContents 物件。
  4. 如果中繼資料欄位有任何變更,可選擇建立 SnapshotMetadataChange 執行個體。
  5. 呼叫 SnapshotsClient.resolveConflict()。在方法呼叫中傳入 SnapshotsClient.SnapshotConflict.getConflictId() 做為第一個引數,並將之前修改的 SnapshotMetadataChangeSnapshotContents 物件分別做為第二個和第三個引數。
  6. 如果 SnapshotsClient.resolveConflict() 呼叫成功,API 會將 Snapshot 物件儲存到伺服器,並嘗試在本機裝置上開啟快照物件。