本指南介绍了如何使用 Google Play 游戏服务提供的 Snapshots API 实现游戏存档功能。这些 API 可在 com.google.android.gms.games.snapshot
和 com.google.android.gms.games
软件包中找到。
准备工作
请阅读已保存的游戏数据游戏概念(如果您尚未阅读)。
- 请务必在 Google Play 管理中心内为您的游戏启用游戏存档支持。
- 在 Android 示例页面中下载并查看游戏存档代码示例。
- 熟悉质量核对清单中所述的建议。
获取 SnapshotsClient
如需开始使用 Snapshots API,您的游戏必须先获取一个 SnapshotsClient
对象。为此,您可以调用 Games.getSnapshotsClient()
方法,并传入 activity 和当前播放器的 GoogleSignInAccount
。如需了解如何检索玩家的账号信息,请参阅 Android 游戏中的登录功能。
指定 Drive 作用域
Snapshots API 依赖于 Google Drive API 来存储游戏存档。如需访问 Drive API,您的应用必须在构建 Google 登录客户端时指定 Drive.SCOPE_APPFOLDER
作用域。
以下示例展示了如何在 onResume()
方法中为您的登录 activity 执行此操作:
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 } } }); }
显示游戏存档
您可以在游戏为玩家提供保存或恢复进度的选项中集成 Snapshot API。您的游戏可以在指定的保存/恢复点显示此类选项,或者允许玩家随时保存或恢复进度。
玩家在游戏中选择保存/恢复选项后,游戏可以选择显示一个屏幕,提示玩家输入新游戏存档的信息,或选择要恢复的现有游戏存档。
为了简化开发,Snapshots API 提供了一个默认游戏存档选择界面 (UI),可供您直接使用。游戏存档选择界面允许玩家创建新的游戏存档、查看现有游戏存档的详细信息以及加载之前的游戏存档。
如需启动默认游戏存档界面,请执行以下操作:
- 调用
SnapshotsClient.getSelectSnapshotIntent()
以获取用于启动默认游戏存档选择界面的Intent
。 - 调用
startActivityForResult()
并传入该Intent
。 如果调用成功,游戏将显示游戏存档选择界面以及您指定的选项。
以下示例展示了如何启动默认游戏存档选择界面:
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); } }); }
如果玩家选择创建新的游戏存档或加载现有的游戏存档,界面会向 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()
异步打开 Snapshot。然后,通过调用SnapshotsClient.DataOrConflict.getData()
从任务结果中检索Snapshot
对象。 - 通过
SnapshotsClient.SnapshotConflict
检索SnapshotContents
实例。 - 调用
SnapshotContents.writeBytes()
以字节格式存储玩家的数据。 - 编写完所有更改后,请调用
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 = Games.getSnapshotsClient(this, GoogleSignIn.getLastSignedInAccount(this)); // Commit the operation return snapshotsClient.commitAndClose(snapshot, metadataChange); }
当您的应用调用 SnapshotsClient.commitAndClose()
时,如果玩家的设备未连接到网络,则 Google Play 游戏服务会将游戏存档数据保存在本地设备上。设备重新连接后,Google Play 游戏服务会将本地缓存的游戏存档更改同步到 Google 的服务器。
加载游戏存档
如需检索当前已登录玩家的游戏存档,请执行以下操作:
- 通过
SnapshotsClient.open()
异步打开 Snapshot。然后,通过调用SnapshotsClient.DataOrConflict.getData()
从任务结果中检索Snapshot
对象。此外,您的游戏也可以通过游戏存档选择界面检索特定 Snapshot,如显示游戏存档中所述。 - 通过
SnapshotsClient.SnapshotConflict
检索SnapshotContents
实例。 - 调用
SnapshotContents.readFully()
以读取 Snapshot 的内容。
以下代码段展示了如何加载特定游戏存档:
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. // ... } }); }
处理游戏存档冲突
在游戏中使用 Snapshot API 时,多个设备可以对同一个游戏存档执行读写操作。如果设备暂时失去网络连接,随后又重新连接,则可能会导致数据冲突,即存储在玩家本地设备上的游戏存档与 Google 的服务器上存储的远程版本不同步。
Snapshot 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 步并重复这些步骤以修改 Snapshot,直到解决冲突为止。 - 如果没有冲突,
SnapshotsClient.DataOrConflict.isConflict()
会返回false
且Snapshot
对象将处于打开状态,可供游戏修改。
- 如果存在冲突,