Android 游戏的游戏存档功能

本指南介绍了如何使用 Google Play 游戏服务提供的 Snapshots API 实现游戏存档功能。这些 API 可在 com.google.android.gms.games.snapshotcom.google.android.gms.games 软件包中找到。

准备工作

如需了解此功能,请参阅游戏存档概览

获取 SnapshotsClient

如需开始使用 Snapshots API,您的游戏必须先获取一个 SnapshotsClient 对象。为此,您可以调用 Games.getSnapshotsContents() 方法并传入 activity。

指定 Drive 作用域

Snapshot API 依赖于 Google Drive API 来存储游戏存档。如需访问 Drive API,您的应用必须在构建 Google 登录客户端时指定 Drive.SCOPE_APPFOLDER 作用域。

以下示例展示了如何在 onResume() 方法中为您的登录 activity 执行此操作:

@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),可供您直接使用。游戏存档选择界面允许玩家创建新的游戏存档、查看现有游戏存档的详细信息以及加载之前的游戏存档。

如需启动默认游戏存档界面,请执行以下操作:

  1. 调用 SnapshotsClient.getSelectSnapshotIntent() 以获取用于启动默认游戏存档选择界面的 Intent
  2. 调用 startActivityForResult() 并传入该 Intent。 如果调用成功,游戏将显示游戏存档选择界面以及您指定的选项。

以下示例展示了如何启动默认游戏存档选择界面:

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);
    }
  });
}

如果玩家选择创建新的游戏存档或加载现有的游戏存档,界面会向 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() 异步打开 Snapshot。

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

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

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

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

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

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

加载游戏存档

如需检索当前已登录玩家的游戏存档,请执行以下操作:

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

  2. 通过调用 SnapshotsClient.DataOrConflict.getData() 从任务结果中检索 Snapshot 对象。此外,您的游戏也可以通过游戏存档选择界面检索特定 Snapshot,如显示游戏存档中所述。

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

  4. 调用 SnapshotContents.readFully() 以读取 Snapshot 的内容。

以下代码段展示了如何加载特定游戏存档:

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.
          // ...
        }
      });
}

处理游戏存档冲突

在游戏中使用 Snapshot API 时,多个设备可以对同一个游戏存档执行读写操作。如果设备暂时失去网络连接,随后又重新连接,则可能会导致数据冲突,即存储在玩家本地设备上的游戏存档与 Google 的服务器上存储的远程版本不同步。

Snapshot 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 对象。