Android 游戏中的游戏存档

本指南介绍了如何使用 Google Play 游戏服务提供的快照 API 实现游戏存档。这些 API 位于 com.google.android.gms.games.snapshotcom.google.android.gms.games 软件包中。

准备工作

有关此功能的信息,请参阅游戏存档概览

获取快照客户端

要开始使用快照 API,您的游戏必须首先获取一个 SnapshotsClient 对象。您可以通过调用 Games.getSnapshotsContents() 方法并传入 activity 来实现。

显示游戏存档

您可以在游戏提供玩家保存或恢复进度的选项的任何地方集成快照 API。您的游戏可以在指定的保存或恢复点显示此类选项,或者允许玩家随时保存或恢复进度。

一旦玩家在游戏中选择保存或恢复选项,您的游戏可以选择弹出一个屏幕,提示玩家输入新游戏存档的信息或选择现有游戏存档进行恢复。

为了简化您的开发,快照 API 提供了一个您可以直接使用的默认游戏存档选择用户界面 (UI)。游戏存档选择 UI 允许玩家创建新游戏存档、查看现有游戏存档的详细信息以及加载以前的游戏存档。

启动默认游戏存档 UI

  1. 调用 SnapshotsClient.getSelectSnapshotIntent() 以获取用于启动默认游戏存档选择 UI 的 Intent
  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 会向 Play 游戏服务发送请求。如果请求成功,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() 异步打开快照。

  2. 通过调用 SnapshotsClient.DataOrConflict.getData() 从任务结果中检索 Snapshot 对象。

  3. 使用 SnapshotsClient.SnapshotConflict 检索 SnapshotContents 实例。

  4. 调用 SnapshotContents.writeBytes() 以字节格式存储玩家数据。

  5. 写入所有更改后,调用 SnapshotsClient.commitAndClose() 将更改发送到 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 =
      PlayGames.getSnapshotsClient(this);

  // Commit the operation
  return snapshotsClient.commitAndClose(snapshot, metadataChange);
}

如果您的应用调用 SnapshotsClient.commitAndClose() 时玩家设备未连接到网络,Play 游戏服务会将游戏存档数据本地存储在设备上。设备重新连接后,Play 游戏服务会将本地缓存的游戏存档更改同步到 Google 服务器。

加载游戏存档

为当前登录的玩家检索游戏存档

  1. 使用 SnapshotsClient.open() 异步打开快照。

  2. 通过调用 SnapshotsClient.DataOrConflict.getData() 从任务结果中检索 Snapshot 对象。或者,您的游戏还可以通过游戏存档选择 UI 检索特定快照,如显示游戏存档中所述。

  3. 使用 SnapshotsClient.SnapshotConflict 检索 SnapshotContents 实例。

  4. 调用 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 提供了一个冲突解决机制,可在读取时呈现两组冲突的游戏存档,并允许您实施适合您游戏的解决策略。

当 Play 游戏服务检测到数据冲突时,SnapshotsClient.DataOrConflict.isConflict() 方法返回 true 值。在这种情况下,SnapshotsClient.SnapshotConflict 类提供了两个版本的游戏存档

  • 服务器版本:Play 游戏服务已知对玩家设备而言是最新的准确版本。

  • 本地版本:在玩家设备之一上检测到的修改版本,其中包含冲突的内容或元数据。这可能与您尝试保存的版本不同。

您的游戏必须决定如何通过选择提供的版本之一或合并两个游戏存档版本的数据来解决冲突。

检测并解决游戏存档冲突

  1. 调用 SnapshotsClient.open()。任务结果包含 SnapshotsClient.DataOrConflict 类。

  2. 调用 SnapshotsClient.DataOrConflict.isConflict() 方法。如果结果为 true,则您有冲突需要解决。

  3. 调用 SnapshotsClient.DataOrConflict.getConflict() 以检索 SnapshotsClient.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 对象存储到服务器,并尝试在您的本地设备上打开 Snapshot 对象。