Hỗ trợ trò chơi đã lưu trong trò chơi Android

Hướng dẫn này cho bạn biết cách triển khai trò chơi đã lưu bằng cách sử dụng API ảnh chụp nhanh do dịch vụ trò chơi của Google Play cung cấp. Bạn có thể tìm thấy API trong các gói com.google.android.gms.games.snapshotcom.google.android.gms.games.

Trước khi bắt đầu

Nếu chưa từng có kinh nghiệm, bạn có thể xem lại các khái niệm về trò chơi đã lưu.

Tải ứng dụng chụp nhanh

Để có thể sử dụng API ảnh chụp nhanh, trước tiên, trò chơi của bạn phải có được một đối tượng SnapshotsClient. Bạn có thể thực hiện việc này bằng cách gọi phương thức Games.getSnapshotsClient() và truyền vào hoạt động.

Chỉ định phạm vi Drive

API ảnh chụp nhanh dựa vào API Google Drive để lưu trữ trò chơi đã lưu. Để truy cập API Drive, ứng dụng của bạn phải chỉ định phạm vi Drive.SCOPE_APPFOLDER khi tạo ứng dụng đăng nhập bằng Google.

Dưới đây là ví dụ về cách thực hiện việc này trong phương thức onResume() cho hoạt động đăng nhập của bạn:


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

Hiển thị trò chơi đã lưu

Bạn có thể tích hợp API ảnh chụp nhanh bất cứ khi nào trò chơi của bạn cung cấp cho người chơi tùy chọn lưu hoặc khôi phục tiến trình của họ. Trò chơi của bạn có thể hiển thị tùy chọn đó tại các điểm lưu/khôi phục được chỉ định, hoặc cho phép người chơi lưu hoặc khôi phục tiến trình bất cứ lúc nào.

Sau khi người chơi chọn tùy chọn lưu/khôi phục cho trò chơi của mình, trò chơi sẽ hiển thị một màn hình nhắc người chơi nhập thông tin cho trò chơi đã lưu mới (nếu muốn), hoặc chọn một trò chơi đã lưu hiện có để khôi phục.

Để đơn giản hóa quá trình phát triển, API ảnh chụp nhanh cung cấp giao diện người dùng (UI) lựa chọn trò chơi đã lưu mặc định mà bạn có thể sử dụng ngay. Giao diện người dùng chọn trò chơi đã lưu cho phép người chơi tạo trò chơi đã lưu mới, xem chi tiết về trò chơi đã lưu hiện có và tải những trò chơi đã lưu trước đó.

Cách mở giao diện người dùng mặc định cho Trò chơi đã lưu:

  1. Hãy gọi SnapshotsClient.getSelectSnapshotIntent() để nhận Intent nhằm chạy giao diện người dùng chọn trò chơi đã lưu mặc định.
  2. Gọi startActivityForResult() và truyền vào đó Intent. Nếu cuộc gọi thành công, trò chơi sẽ hiển thị giao diện người dùng cho trò chơi đã lưu, cùng với các tùy chọn mà bạn chỉ định.

Dưới đây là một ví dụ về cách khởi chạy giao diện người dùng lựa chọn trò chơi đã lưu mặc định:

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

Nếu người chơi chọn tạo một trò chơi đã lưu mới hoặc tải trò chơi đã lưu hiện có, giao diện người dùng sẽ gửi yêu cầu đến dịch vụ trò chơi của Google Play. Nếu yêu cầu thành công, Dịch vụ trò chơi của Google Play sẽ trả về thông tin để tạo hoặc khôi phục trò chơi đã lưu thông qua lệnh gọi lại onActivityResult(). Trò chơi của bạn có thể ghi đè lệnh gọi lại này để kiểm tra xem liệu có lỗi nào xảy ra trong yêu cầu hay không.

Đoạn mã sau đây cho thấy quy trình triển khai mẫu của 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
      // ...
    }
  }
}

Viết trò chơi đã lưu

Cách lưu trữ nội dung vào trò chơi đã lưu:

  1. Mở ảnh chụp nhanh không đồng bộ qua SnapshotsClient.open(). Sau đó, truy xuất đối tượng Snapshot từ kết quả của tác vụ bằng cách gọi SnapshotsClient.DataOrConflict.getData().
  2. Truy xuất bản sao SnapshotContents qua SnapshotsClient.SnapshotConflict.
  3. Gọi SnapshotContents.writeBytes() để lưu trữ dữ liệu của người chơi ở định dạng byte.
  4. Khi những thay đổi này đã được viết ra, hãy gọi SnapshotsClient.commitAndClose() để gửi những thay đổi đó tới máy chủ của Google. Trong cuộc gọi phương thức, trò chơi của bạn có thể cung cấp thêm thông tin để cho Dịch vụ trò chơi của Google Play biết cách hiển thị trò chơi đã lưu này cho người chơi. Thông tin này được thể hiện ở đối tượng SnapshotMetaDataChange, là đối tượng mà trò chơi của bạn sử dụng SnapshotMetadataChange.Builder để tạo.

Đoạn mã sau đây cho biết cách trò chơi của bạn có thể áp dụng thay đổi cho trò chơi đã lưu:

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

Nếu thiết bị của người chơi chưa kết nối mạng khi ứng dụng của bạn gọi SnapshotsClient.commitAndClose(), các dịch vụ trò chơi của Google Play sẽ lưu trữ dữ liệu trò chơi đã lưu trên thiết bị. Sau khi kết nối lại với thiết bị, dịch vụ trò chơi của Google Play sẽ đồng bộ hóa những thay đổi đối với trò chơi đã lưu trên bộ nhớ đệm của thiết bị vào máy chủ Google.

Đang tải trò chơi đã lưu

Cách truy xuất trò chơi đã lưu cho người chơi hiện đang đăng nhập:

  1. Mở ảnh chụp nhanh không đồng bộ qua SnapshotsClient.open(). Sau đó, truy xuất đối tượng Snapshot từ kết quả của tác vụ bằng cách gọi SnapshotsClient.DataOrConflict.getData(). Ngoài ra, trò chơi của bạn cũng có thể truy xuất một ảnh chụp nhanh cụ thể thông qua giao diện người dùng chọn trò chơi đã lưu, như được mô tả trong nội dung phần Hiển thị trò chơi đã lưu.
  2. Truy xuất bản sao SnapshotContents qua SnapshotsClient.SnapshotConflict.
  3. Hãy gọi SnapshotContents.readFully() để đọc nội dung của ảnh chụp nhanh.

Đoạn mã sau đây minh họa cách tải trò chơi đã lưu cụ thể:

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

Xử lý xung đột với trò chơi đã lưu

Khi sử dụng API ảnh chụp nhanh trong ứng dụng trò chơi, nhiều thiết bị có thể đọc và ghi trên cùng một trò chơi đã lưu. Trong trường hợp thiết bị tạm thời mất kết nối mạng và sau đó kết nối lại, việc này có thể gây ra xung đột dữ liệu khi trò chơi đã lưu trên thiết bị cục bộ của người chơi không đồng bộ với phiên bản lưu trữ từ xa trong máy chủ của Google.

API ảnh chụp nhanh cung cấp cơ chế giải quyết xung đột đại diện cho cả hai nhóm trò chơi đã lưu xung đột nhau tại thời điểm đọc, đồng thời cho phép bạn triển khai chiến lược xử lý phù hợp với trò chơi của mình.

Khi phát hiện xung đột dữ liệu trong Dịch vụ trò chơi của Google Play, phương thức SnapshotsClient.DataOrConflict.isConflict() sẽ trả về giá trị true. Trong trường hợp này, lớp SnapshotsClient.SnapshotConflict cung cấp hai phiên bản của trò chơi đã lưu:

  • Phiên bản máy chủ: là phiên bản mới nhất mà Dịch vụ trò chơi của Google Play biết đến là chính xác cho thiết bị của người chơi; và
  • Phiên bản cục bộ: Phiên bản sửa đổi được phát hiện trên một trong các thiết bị của người chơi chứa siêu dữ liệu hoặc nội dung xung đột. Phiên bản này có thể không giống với phiên bản mà bạn đã cố lưu lại.

Để giải quyết xung đột, trò chơi của bạn phải quyết định chọn một trong những phiên bản được cung cấp hoặc hợp nhất dữ liệu của hai phiên bản trò chơi đã lưu.

Cách phát hiện và giải quyết các xung đột trò chơi đã lưu:

  1. Gọi SnapshotsClient.open(). Kết quả tác vụ phải chứa một lớp SnapshotsClient.DataOrConflict.
  2. Gọi phương thức SnapshotsClient.DataOrConflict.isConflict(). Nếu kết quả là đúng, bạn hiện có một xung đột cần giải quyết.
  3. Gọi SnapshotsClient.DataOrConflict.getConflict() để truy xuất bản sao SnaphotsClient.snapshotConflict.
  4. Gọi SnapshotsClient.SnapshotConflict.getConflictId() để truy xuất mã xung đột, là giá trị nhận dạng duy nhất xung đột đã phát hiện. Trò chơi của bạn cần có giá trị này để gửi yêu cầu giải quyết xung đột sau.
  5. Gọi SnapshotsClient.SnapshotConflict.getConflictingSnapshot() để tải phiên bản cục bộ.
  6. Gọi SnapshotsClient.SnapshotConflict.getSnapshot() để tải phiên bản máy chủ.
  7. Để giải quyết xung đột cho trò chơi đã lưu, hãy chọn một phiên bản mà bạn muốn lưu vào máy chủ làm phiên bản cuối cùng, đồng thời chuyển sang phương thức SnapshotsClient.resolveConflict().

Đoạn mã sau đây giải thích và đưa ra ví dụ về cách trò chơi của bạn xử lý các xung đột qua việc chọn trò chơi đã lưu được sửa đổi gần đây nhất làm phiên bản cuối cùng để lưu:


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

Sửa đổi trò chơi đã lưu để giải quyết xung đột

Nếu bạn muốn hợp nhất dữ liệu từ nhiều trò chơi đã lưu, hoặc sửa đổi một Snapshot hiện có để lưu vào máy chủ dưới dạng phiên bản cuối cùng đã giải quyết, vui lòng làm theo các bước sau:

  1. Gọi SnapshotsClient.open() .
  2. Hãy gọi SnapshotsClient.SnapshotConflict.getResolutionSnapshotsContent() để nhận đối tượng SnapshotContents mới.
  3. Hợp nhất dữ liệu từ SnapshotsClient.SnapshotConflict.getConflictingSnapshot()SnapshotsClient.SnapshotConflict.getSnapshot() vào đối tượng SnapshotContents ở bước trước.
  4. Bạn có thể tạo một bản sao SnapshotMetadataChange nếu có bất kỳ thay đổi nào đối với các trường siêu dữ liệu.
  5. Gọi SnapshotsClient.resolveConflict(). Trong lệnh gọi phương thức, hãy truyền SnapshotsClient.SnapshotConflict.getConflictId() làm đối số đầu tiên và các đối tượng SnapshotMetadataChangeSnapshotContents mà bạn đã sửa đổi trước đó làm đối số thứ hai và thứ ba tương ứng.
  6. Nếu lệnh gọi SnapshotsClient.resolveConflict() thành công, API sẽ lưu trữ đối tượng Snapshot vào máy chủ và cố gắng mở đối tượng Chụp nhanh trên thiết bị cục bộ.
    • Nếu có xung đột, SnapshotsClient.DataOrConflict.isConflict() sẽ trả về true. Trong trường hợp này, trò chơi của bạn sẽ quay lại bước 2 và lặp lại các bước để sửa đổi ảnh chụp nhanh cho đến khi các xung đột được giải quyết.
    • Nếu không có xung đột nào, SnapshotsClient.DataOrConflict.isConflict() sẽ trả về false và đối tượng Snapshot sẽ mở để trò chơi của bạn sửa được.