本指南将向您展示如何使用 Google Play 游戏服务提供的快照 API 实现存档游戏。这些 API 位于 com.google.android.gms.games.snapshot
和 com.google.android.gms.games
包中。
开始之前
如果您尚未这样做,您可能会发现查看 存档游戏概念 很有帮助。
- 确保在 Google Play Console 中为您的游戏 启用存档游戏支持。
- 下载并在 Android 示例页面 中查看存档游戏代码示例。
- 熟悉 质量检查表 中描述的建议。
获取快照客户端
要开始使用快照 API,您的游戏必须首先获取 SnapshotsClient
对象。您可以通过调用 Games.getSnapshotsClient()
方法并传入活动和当前玩家的 GoogleSignInAccount
来执行此操作。要了解如何检索玩家帐户信息,请参阅 Android 游戏中的登录。
指定 Drive 范围
快照 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()
以获取用于启动默认存档游戏选择 UI 的Intent
。 - 调用
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
对象存储到服务器,并尝试在您的本地设备上打开 Snapshot 对象。- 如果存在冲突,
SnapshotsClient.DataOrConflict.isConflict()
返回true
。在这种情况下,您的游戏应返回到步骤 2 并重复修改快照的步骤,直到冲突得到解决。 - 如果不存在冲突,
SnapshotsClient.DataOrConflict.isConflict()
返回false
,并且Snapshot
对象已打开,以便您的游戏进行修改。
- 如果存在冲突,