Android 游戏中的游戏存档支持

本指南介绍了如何使用 快照 API。这些 API 可在 com.google.android.gms.games.snapshotcom.google.android.gms.games 软件包中找到。

准备工作

建议您查看 游戏存档游戏概念

获取快照客户端

如需开始使用 Snapshots API,您的游戏必须先获取一个 SnapshotsClient 对象。为此,您可以调用 Games.getSnapshotsClient() 方法并传入 activity 以及当前玩家的 GoogleSignInAccount。要了解如何 检索玩家的账号信息,请参阅 在 Android 游戏中登录

指定云端硬盘范围

Snapshot API 依赖于 Google Drive API 来存储游戏存档。接收者 访问 Drive API,您的应用必须指定 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),可供您直接使用。游戏存档选择界面允许玩家创建新的游戏存档、查看现有游戏存档的详细信息以及加载之前的游戏存档。

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

  1. 调用 SnapshotsClient.getSelectSnapshotIntent() 以获取 Intent(用于启动默认设置) 游戏存档选择界面
  2. 调用 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
      // ...
    }
  }
}

编写游戏存档

如需将内容保存到游戏存档,请执行以下操作:

  1. 通过 SnapshotsClient.open() 异步打开快照。然后,检索 Snapshot 对象 从任务结果中调用 SnapshotsClient.DataOrConflict.getData()
  2. 通过 SnapshotsClient.SnapshotConflict 检索 SnapshotContents 实例。
  3. 调用 SnapshotContents.writeBytes() 以字节格式存储播放器的数据。
  4. 编写好所有更改后,调用 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 服务器进行更改。

正在加载游戏存档

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

  1. 通过 SnapshotsClient.open() 异步打开快照。然后,检索 Snapshot 对象 从任务结果中调用 SnapshotsClient.DataOrConflict.getData()。或者,您的 游戏还可以通过游戏存档选择界面检索特定快照,如 显示游戏存档
  2. 通过 SnapshotsClient.SnapshotConflict 检索 SnapshotContents 实例。
  3. 调用 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.
          // ...
        }
      });
}

处理游戏存档冲突

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

Snapshot API 提供了一种冲突解决机制,该机制会在读取时呈现这两组有冲突的游戏存档,并且可允许您实现适合您的游戏的解决策略。

当 Google Play 游戏服务检测到数据冲突时, SnapshotsClient.DataOrConflict.isConflict() 方法会返回 true 值。在此事件中, SnapshotsClient.SnapshotConflict 类提供了游戏存档的两个版本:

  • 服务器版本:Google Play 游戏服务公认的最新版本 提供和
  • 本地版本:在玩家的一台设备上检测到的经过修改的版本,其中包含 冲突的内容或元数据。这可能与您尝试保存的版本不同。

您的游戏必须决定如何解决冲突,具体方式包括选择所提供的某个版本或合并两个游戏存档版本的数据。

如需检测并解决游戏存档问题,请执行以下操作:

  1. 调用 SnapshotsClient.open()。任务结果包含 SnapshotsClient.DataOrConflict 类。
  2. 调用 SnapshotsClient.DataOrConflict.isConflict() 方法。如果结果为 true,您将得到 冲突解决。
  3. 调用 SnapshotsClient.DataOrConflict.getConflict() 来检索 SnaphotsClient.snapshotConflict 实例。
  4. 调用 SnapshotsClient.SnapshotConflict.getConflictId() 来检索唯一 识别检测到的冲突。您的游戏需要使用此值才能发送冲突解决请求 。
  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 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 若要保存到服务器中的解析的最终版本,请按照以下步骤操作:

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