在 Android 游戏中支持保存的游戏

Google 登录 API 弃用后,我们将在 2026 年移除游戏 v1 SDK。在 2025 年 2 月之后,您将无法在 Google Play 上发布新集成游戏 v1 SDK 的游戏。我们建议您改用游戏 v2 SDK。
虽然现有采用之前游戏 v1 集成的游戏在未来几年内仍可继续运行,但我们鼓励您从 2025 年 6 月开始迁移到 v2
本指南介绍如何使用 Play Games Services v1 SDK 实现保存的游戏。有关最新 SDK 版本的信息,请参阅v2 文档

本指南向您展示如何使用 Google Play 游戏服务提供的快照 API 实现保存的游戏。API 位于 com.google.android.gms.games.snapshotcom.google.android.gms.games 软件包中。

开始之前

如果您尚未这样做,复习一下保存的游戏游戏概念可能会有所帮助。

获取快照客户端

要开始使用快照 API,您的游戏必须首先获取一个 SnapshotsClient 对象。您可以通过调用 Games.getSnapshotsClient() 方法并将 Activity 和当前玩家的 GoogleSignInAccount 传递进去来完成此操作。要了解如何检索玩家帐号信息,请参阅在 Android 游戏中登录

指定 Drive 范围

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

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

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

显示保存的游戏

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

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

为了简化您的开发,快照 API 提供了一个默认的保存游戏选择用户界面 (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 =
      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);
    }
  });
}

如果玩家选择创建新的保存游戏或加载现有的保存游戏,UI 会向 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() 异步打开快照。然后,通过调用 SnapshotsClient.DataOrConflict.getData() 从任务结果中检索 Snapshot 对象。
  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() 异步打开快照。然后,通过调用 SnapshotsClient.DataOrConflict.getData() 从任务结果中检索 Snapshot 对象。或者,您的游戏还可以通过保存游戏选择 UI 检索特定的快照,如显示保存的游戏中所述。
  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.
          // ...
        }
      });
}

处理保存的游戏冲突

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

快照 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() 检索一个 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 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 对象。