Сохраненные игры для Android-игр.

В этом руководстве показано, как реализовать сохранение игр с помощью API снимков, предоставляемого сервисами Google Play Games. API можно найти в пакетах com.google.android.gms.games.snapshot и com.google.android.gms.games .

Прежде чем начать

Подробную информацию об этой функции см. в разделе « Обзор сохраненных игр» .

Загрузите клиент для создания снимков.

Для начала использования API снимков экрана ваша игра должна получить объект SnapshotsClient . Это можно сделать, вызвав метод Games.getSnapshotsContents() и передав в него объект Activity.

Отображение сохраненных игр

Вы можете интегрировать API снимков прогресса везде, где ваша игра предоставляет игрокам возможность сохранять или восстанавливать свой прогресс. Ваша игра может отображать такую ​​опцию в специально отведенных точках сохранения или восстановления или позволять игрокам сохранять или восстанавливать прогресс в любое время.

После того, как игроки выберут опцию сохранения или восстановления в вашей игре, может появиться экран, предлагающий ввести информацию для нового сохраненного файла или выбрать существующий сохраненный файл для восстановления.

Для упрощения разработки API снимков предоставляет стандартный пользовательский интерфейс выбора сохраненных игр, который можно использовать сразу же. Интерфейс выбора сохраненных игр позволяет игрокам создавать новые сохраненные игры, просматривать подробную информацию о существующих сохраненных играх и загружать предыдущие сохраненные игры.

Чтобы запустить стандартный интерфейс сохраненных игр:

  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 Games Services. В случае успешного запроса Play Games Services возвращает информацию для создания или восстановления сохранённого файла через функцию обратного вызова 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. Получите объект Snapshot из результата задачи, вызвав метод SnapshotsClient.DataOrConflict.getData() .

  3. Получите экземпляр SnapshotContents с помощью SnapshotsClient.SnapshotConflict .

  4. Вызовите SnapshotContents.writeBytes() для сохранения данных игрока в байтовом формате.

  5. После записи всех изменений вызовите метод SnapshotsClient.commitAndClose() , чтобы отправить изменения на серверы Google. В вызове метода ваша игра может дополнительно указать Play Games Services, как отображать сохраненную игру игрокам. Эта информация представлена ​​в объекте 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 Games Services сохраняет данные игры локально на устройстве. После повторного подключения устройства Play Games Services синхронизирует локально кэшированные изменения игры с серверами Google.

Загрузить сохраненные игры

Чтобы получить доступ к сохраненным играм для авторизованного игрока:

  1. Асинхронно откройте снимок с помощью SnapshotsClient.open() .

  2. Объект Snapshot можно получить из результата задачи, вызвав метод SnapshotsClient.DataOrConflict.getData() . В качестве альтернативы, ваша игра также может получить конкретный снимок через пользовательский интерфейс выбора сохраненных игр, как описано в разделе «Отображение сохраненных игр» .

  3. Получите экземпляр SnapshotContents с помощью SnapshotsClient.SnapshotConflict .

  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 Games Services обнаруживает конфликт данных, метод SnapshotsClient.DataOrConflict.isConflict() возвращает значение true В этом случае класс SnapshotsClient.SnapshotConflict предоставляет две версии сохраненной игры:

  • Версия сервера : Самая актуальная версия, известная Play Games Services как подходящая для устройства игрока.

  • Локальная версия : Измененная версия, обнаруженная на одном из устройств игрока, содержащая конфликтующее содержимое или метаданные. Она может отличаться от версии, которую вы пытались сохранить.

Вашей игре предстоит решить, как разрешить конфликт, выбрав одну из предложенных версий или объединив данные двух сохраненных версий игры.

Для обнаружения и разрешения конфликтов сохранений игры:

  1. Вызовите метод SnapshotsClient.open() . Результатом выполнения задачи станет объект класса SnapshotsClient.DataOrConflict .

  2. Вызовите метод SnapshotsClient.DataOrConflict.isConflict() . Если результат истинный, значит, у вас есть конфликт, который необходимо разрешить.

  3. Вызовите SnapshotsClient.DataOrConflict.getConflict() чтобы получить экземпляр SnapshotsClient.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 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() , а в качестве второго и третьего аргументов — объекты SnapshotMetadataChange и SnapshotContents , которые вы изменили ранее, соответственно.

  6. Если вызов SnapshotsClient.resolveConflict() будет успешным, API сохранит объект Snapshot на сервере и попытается открыть объект Snapshot на вашем локальном устройстве.

    • Если возникает конфликт, SnapshotsClient.DataOrConflict.isConflict() возвращает true . В этом случае ваша игра должна вернуться к шагу 2 и повторять шаги по изменению снимка до тех пор, пока конфликты не будут разрешены.
    • Если конфликта нет, SnapshotsClient.DataOrConflict.isConflict() возвращает false , и объект Snapshot становится доступным для изменения вашей игрой.