Android 게임용 저장된 게임

이 가이드에서는 Google Play 게임즈 서비스에서 제공하는 Snapshots API를 사용하여 저장된 게임을 구현하는 방법을 보여줍니다. 이 API는 com.google.android.gms.games.snapshotcom.google.android.gms.games 패키지에서 확인할 수 있습니다.

시작하기 전에

이 기능에 관한 자세한 내용은 저장된 게임 개요를 참고하세요.

스냅샷 클라이언트 가져오기

Snapshots API를 사용하려면 먼저 게임에서 SnapshotsClient 객체를 가져와야 합니다. 이렇게 하려면 Games.getSnapshotsContents() 메서드를 호출하고 활동을 전달하면 됩니다.

드라이브 범위 지정

Snapshots API는 저장된 게임 저장소에 Google Drive API를 사용합니다. Drive API에 액세스하려면 앱이 Google 로그인 클라이언트를 빌드할 때 Drive.SCOPE_APPFOLDER 범위를 지정해야 합니다.

다음은 로그인 활동의 onResume() 메서드에서 이를 실행하는 방법을 보여주는 예입니다.

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

저장된 게임 표시

게임에서 플레이어에게 진행 상황을 저장하거나 복원하는 옵션을 제공할 때마다 Snapshots API를 통합할 수 있습니다. 게임은 지정된 저장 또는 복원 지점에 이러한 옵션을 표시하거나 플레이어가 언제든지 진행 상황을 저장하거나 복원하도록 허용할 수 있습니다.

플레이어가 게임에서 저장 또는 복원 옵션을 선택하면 게임에서는 선택적으로 새로 저장된 게임의 정보를 입력하거나 복원할 기존 저장된 게임을 선택하라는 메시지가 담긴 화면을 표시할 수 있습니다.

개발을 간소화하기 위해 Snapshots API는 즉시 사용할 수 있는 기본 저장된 게임 선택 사용자 인터페이스 (UI)를 제공합니다. 저장된 게임 선택 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 =
      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);
    }
  });
}

플레이어가 새 저장된 게임을 만들거나 기존 저장된 게임을 로드하도록 선택하면 UI에서는 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()를 사용하여 스냅샷을 비동기식으로 엽니다.

  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()을 사용하여 스냅샷을 비동기식으로 엽니다.

  2. SnapshotsClient.DataOrConflict.getData()를 호출하여 작업의 결과에서 Snapshot 객체를 가져옵니다. 또는 저장된 게임 표시에 설명된 대로 저장된 게임 선택 UI를 통해 게임이 특정 스냅샷을 가져올 수도 있습니다.

  3. SnapshotsClient.SnapshotConflict를 사용하여 SnapshotContents 인스턴스를 가져옵니다.

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

저장된 게임 충돌 처리

게임에서 Snapshots API를 사용하면 여러 기기가 하나의 저장된 게임에서 읽기 및 쓰기를 실행할 수 있습니다. 기기의 네트워크 연결이 일시적으로 끊어졌다가 나중에 다시 연결되면 플레이어의 로컬 기기에 저장된 게임이 Google 서버에 저장된 원격 버전과 동기화되지 않는 데이터 충돌이 발생할 수 있습니다.

Snapshots 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 객체를 열려고 시도합니다.