Android 游戏的已保存游戏

本指南向您展示如何使用 Google Play 游戏服务提供的快照 API 实现已保存的游戏。这些 API 可以在 com.google.android.gms.games.snapshotcom.google.android.gms.games 包中找到。

开始之前

有关此功能的信息,请参阅 已保存的游戏概述

获取快照客户端

要开始使用快照 API,您的游戏必须首先获取一个 SnapshotsClient 对象。您可以通过调用 Games.getSnapshotsClient() 方法并传入活动来执行此操作。

指定驱动器范围

快照 API 依赖于 Google Drive API 进行已保存的游戏存储。要访问 Drive API,您的应用必须在构建 Google 登录客户端时指定 Drive.SCOPE_APPFOLDER 范围。

以下是如何在登录活动的 onResume() 方法中执行此操作的示例

@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() 以获取用于启动默认已保存游戏选择 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() 以检索 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 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 对象。