本指南說明如何使用 Google Play 遊戲服務提供的快照 API 加入遊戲進度存檔。您可以在 com.google.android.gms.games.snapshot
和 com.google.android.gms.games
套件中取得此 API。
事前準備
建議您先複習儲存遊戲遊戲概念,這會對您很有幫助。
- 請務必在 Google Play 管理中心啟用遊戲進度存檔支援。
- 請至 Android 範例頁面下載和檢閱遊戲進度存檔程式碼範例。
- 熟悉「品質檢查清單」中所列的建議。
取得快照用戶端
如要開始使用快照 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:
- 呼叫
SnapshotsClient.getSelectSnapshotIntent()
取得Intent
,以啟動預設的遊戲進度存檔選擇 UI。 - 呼叫
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 // ... } } }
寫入遊戲進度存檔
在遊戲進度存檔中儲存內容:
- 透過
SnapshotsClient.open()
,以非同步方式開啟快照。然後呼叫SnapshotsClient.DataOrConflict.getData()
,從工作結果中擷取Snapshot
物件。 - 透過
SnapshotsClient.SnapshotConflict
擷取SnapshotContents
例項。 - 呼叫
SnapshotContents.writeBytes()
,以位元組格式儲存玩家的資料。 - 寫入所有變更後,請呼叫
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 的伺服器。
載入遊戲進度存檔
擷取目前登入玩家的遊戲進度存檔:
- 透過
SnapshotsClient.open()
,以非同步方式開啟快照。接著,呼叫SnapshotsClient.DataOrConflict.getData()
,從工作結果中擷取Snapshot
物件。此外,遊戲也可以透過遊戲進度存檔選擇 UI 擷取特定快照,如「顯示遊戲進度存檔」所述。 - 透過
SnapshotsClient.SnapshotConflict
擷取SnapshotContents
例項。 - 呼叫
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 遊戲服務已知的最新版本,為玩家的裝置提供準確資訊。
- 本機版本:在其中一部玩家裝置中偵測到修改過的版本,而該版本包含有衝突的內容或中繼資料。這可能與您嘗試儲存的版本不同。
您的遊戲必須決定如何解決衝突問題:選擇其中一個版本,或合併兩個遊戲進度存檔版本的資料。
偵測並解決遊戲進度存檔衝突問題:
- 呼叫
SnapshotsClient.open()
。工作結果包含SnapshotsClient.DataOrConflict
類別。 - 呼叫
SnapshotsClient.DataOrConflict.isConflict()
方法。如果結果為 true,就必須解決衝突。 - 呼叫
SnapshotsClient.DataOrConflict.getConflict()
以擷取SnaphotsClient.snapshotConflict
例項。 - 呼叫
SnapshotsClient.SnapshotConflict.getConflictId()
以擷取偵測到的衝突專屬 (不重複) 的衝突 ID。遊戲稍後需要使用此值傳送衝突解決要求。 - 呼叫
SnapshotsClient.SnapshotConflict.getConflictingSnapshot()
即可取得本機版本。 - 呼叫
SnapshotsClient.SnapshotConflict.getSnapshot()
即可取得伺服器版本。 - 如要解決遊戲進度存檔衝突,請選取要儲存至伺服器的版本做為最終版本,然後將該版本傳遞至
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
做為已解決問題的最終版本以儲存至伺服器,請按照下列步驟操作:
- 呼叫
SnapshotsClient.open()
。 - 呼叫
SnapshotsClient.SnapshotConflict.getResolutionSnapshotsContent()
以取得新的SnapshotContents
物件。 - 將
SnapshotsClient.SnapshotConflict.getConflictingSnapshot()
和SnapshotsClient.SnapshotConflict.getSnapshot()
的資料合併至上一步的SnapshotContents
物件。 - 或者,如果中繼資料欄位有任何變更,也可建立
SnapshotMetadataChange
例項。 - 呼叫
SnapshotsClient.resolveConflict()
。在方法呼叫中,將SnapshotsClient.SnapshotConflict.getConflictId()
傳遞為第一個引數,然後傳遞之前修改為第二和第三個引數的SnapshotMetadataChange
和SnapshotContents
物件。 - 如果
SnapshotsClient.resolveConflict()
呼叫成功,API 會在伺服器中儲存Snapshot
物件,並嘗試在本機裝置中開啟快照物件。- 如果有衝突,
SnapshotsClient.DataOrConflict.isConflict()
會傳回true
。遇到這種情況時,遊戲應返回步驟 2,然後重複操作步驟以修改快照,直到衝突解決為止。 - 如果沒有任何衝突,
SnapshotsClient.DataOrConflict.isConflict()
會傳回false
,且Snapshot
物件也會開放,可供遊戲修改。
- 如果有衝突,