Android 遊戲的遊戲進度存檔支援功能

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

事前準備

建議您先複習儲存遊戲遊戲概念,這會對您很有幫助。

取得快照用戶端

如要開始使用快照 API,遊戲必須先取得 SnapshotsClient 物件。方法是呼叫 Games.getSnapshotsClient() 方法,然後傳入活動和目前播放器的 GoogleSignInAccount。如要瞭解如何擷取玩家帳戶資訊,請參閱「Android 遊戲登入」。

指定雲端硬碟範圍

快照 API 需要使用 Google Drive API 才能儲存遊戲進度存檔。如要存取 Drive API,應用程式必須在建立 Google 登入用戶端時指定 Drive.SCOPE_APPFOLDER 範圍。

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

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

顯示遊戲進度存檔

您可以在遊戲的任何位置整合加入快照 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 =
      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);
    }
  });
}

如果玩家選擇建立新的遊戲進度存檔,或載入現有的遊戲進度存檔,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 物件表示,而這是遊戲使用 SnapshotMetadataChange.Builder 建立的物件。

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

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

當應用程式呼叫 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 =
      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.
          // ...
        }
      });
}

處理遊戲進度存檔衝突

在遊戲中使用快照 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 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");
              }
            }
          });
}

修改遊戲進度存檔以解決衝突問題

如果要合併多個遊戲進度存檔的資料,或修改現有的 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 物件,並嘗試在本機裝置中開啟快照物件。