پشتیبانی از بازی های ذخیره شده در بازی های اندروید

پس از منسوخ شدن رابط برنامه‌نویسی کاربردی ورود گوگل (Google Sign-In API)، ما در سال ۲۰۲۶ کیت توسعه نرم‌افزاری بازی‌ها نسخه ۱ (games v1 SDK) را حذف خواهیم کرد. پس از فوریه ۲۰۲۵، شما قادر به انتشار عناوینی که به تازگی با کیت توسعه نرم‌افزاری بازی‌ها نسخه ۱ (games v1 SDK) ادغام شده‌اند، در گوگل پلی نخواهید بود. توصیه می‌کنیم به جای آن از کیت توسعه نرم‌افزاری بازی‌ها نسخه ۲ (games v2 SDK) استفاده کنید.
در حالی که عناوین موجود با بازی‌های قبلی نسخه ۱ ادغام‌شده تا چند سال دیگر به کار خود ادامه می‌دهند، توصیه می‌شود از ژوئن ۲۰۲۵ به نسخه ۲ مهاجرت کنید .
این راهنما برای استفاده از SDK نسخه ۱ سرویس بازی‌های Play Games Services است. برای اطلاعات بیشتر در مورد آخرین نسخه SDK، به مستندات نسخه ۲ مراجعه کنید.

این راهنما به شما نشان می‌دهد که چگونه بازی‌های ذخیره شده را با استفاده از API اسنپ‌شات ارائه شده توسط سرویس‌های بازی‌های گوگل پلی پیاده‌سازی کنید. این APIها را می‌توانید در بسته‌های com.google.android.gms.games.snapshot و com.google.android.gms.games پیدا کنید.

قبل از اینکه شروع کنی

اگر قبلاً این کار را نکرده‌اید، ممکن است مرور مفاهیم بازی بازی‌های ذخیره‌شده (Saved Games) برایتان مفید باشد.

کلاینت اسنپ‌شات‌ها را دریافت کنید

برای شروع استفاده از API مربوط به snapshots، بازی شما ابتدا باید یک شیء SnapshotsClient دریافت کند. می‌توانید این کار را با فراخوانی متد Games.getSnapshotsClient() و ارسال activity و GoogleSignInAccount برای بازیکن فعلی انجام دهید. برای یادگیری نحوه بازیابی اطلاعات حساب بازیکن، به بخش ورود به سیستم در بازی‌های اندروید مراجعه کنید.

محدوده درایو را مشخص کنید

API مربوط به snapshots برای ذخیره‌سازی بازی‌های ذخیره شده به API گوگل درایو متکی است. برای دسترسی به API درایو، برنامه شما باید هنگام ساخت کلاینت ورود به سیستم گوگل، محدوده Drive.SCOPE_APPFOLDER را مشخص کند.

در اینجا مثالی از نحوه انجام این کار در متد 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 مربوط به snapshots را در هر جایی که بازی شما به بازیکنان امکان ذخیره یا بازیابی پیشرفتشان را می‌دهد، ادغام کنید. بازی شما ممکن است چنین گزینه‌ای را در نقاط ذخیره/بازیابی تعیین‌شده نمایش دهد یا به بازیکنان اجازه دهد تا پیشرفت خود را در هر زمانی ذخیره یا بازیابی کنند.

وقتی بازیکنان گزینه ذخیره/بازیابی را در بازی شما انتخاب می‌کنند، بازی شما می‌تواند به صورت اختیاری صفحه‌ای را نمایش دهد که از بازیکنان می‌خواهد اطلاعات مربوط به یک بازی ذخیره شده جدید را وارد کنند یا یک بازی ذخیره شده موجود را برای بازیابی انتخاب کنند.

برای ساده‌سازی توسعه، API مربوط به snapshots یک رابط کاربری (UI) پیش‌فرض برای انتخاب بازی‌های ذخیره‌شده ارائه می‌دهد که می‌توانید از آن به صورت آماده استفاده کنید. رابط کاربری انتخاب بازی‌های ذخیره‌شده به بازیکنان اجازه می‌دهد تا یک بازی ذخیره‌شده جدید ایجاد کنند، جزئیات مربوط به بازی‌های ذخیره‌شده موجود را مشاهده کنند و بازی‌های ذخیره‌شده قبلی را بارگذاری کنند.

برای اجرای رابط کاربری پیش‌فرض بازی‌های ذخیره‌شده:

  1. برای دریافت یک Intent جهت اجرای رابط کاربری انتخاب بازی‌های ذخیره شده‌ی پیش‌فرض، تابع SnapshotsClient.getSelectSnapshotIntent() را فراخوانی کنید.
  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);
    }
  });
}

اگر بازیکن تصمیم به ایجاد یک بازی ذخیره شده جدید یا بارگذاری یک بازی ذخیره شده موجود بگیرد، رابط کاربری درخواستی را به سرویس‌های بازی‌های گوگل پلی ارسال می‌کند. در صورت موفقیت‌آمیز بودن درخواست، سرویس‌های بازی‌های گوگل پلی اطلاعات لازم برای ایجاد یا بازیابی بازی ذخیره شده را از طریق فراخوانی 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. به صورت ناهمگام یک snapshot را از طریق SnapshotsClient.open() باز کنید. سپس، شیء Snapshot را از نتیجه task با فراخوانی SnapshotsClient.DataOrConflict.getData() بازیابی کنید.
  2. یک نمونه SnapshotContents را از طریق SnapshotsClient.SnapshotConflict بازیابی کنید.
  3. برای ذخیره داده‌های بازیکن در قالب بایت، تابع SnapshotContents.writeBytes() را فراخوانی کنید.
  4. پس از نوشتن تمام تغییرات، برای ارسال تغییرات به سرورهای گوگل، SnapshotsClient.commitAndClose() را فراخوانی کنید. در فراخوانی این متد، بازی شما می‌تواند به صورت اختیاری اطلاعات اضافی ارائه دهد تا به سرویس‌های بازی‌های گوگل پلی بگوید که چگونه این بازی ذخیره شده را به بازیکنان ارائه دهند. این اطلاعات در یک شیء 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() توسط برنامه شما به شبکه متصل نباشد، سرویس‌های بازی‌های گوگل پلی داده‌های بازی ذخیره شده را به صورت محلی در دستگاه ذخیره می‌کنند. پس از اتصال مجدد دستگاه، سرویس‌های بازی‌های گوگل پلی تغییرات بازی ذخیره شده در حافظه پنهان محلی را با سرورهای گوگل همگام‌سازی می‌کنند.

بازی‌های ذخیره شده را بارگیری کنید

برای بازیابی بازی‌های ذخیره شده برای بازیکنی که در حال حاضر وارد سیستم شده است:

  1. به صورت ناهمگام یک snapshot را از طریق SnapshotsClient.open() باز کنید. سپس، شیء Snapshot را از نتیجه‌ی وظیفه با فراخوانی SnapshotsClient.DataOrConflict.getData() بازیابی کنید. به عنوان یک روش جایگزین، بازی شما می‌تواند یک snapshot خاص را از طریق رابط کاربری انتخاب بازی‌های ذخیره شده، همانطور که در بخش نمایش بازی‌های ذخیره شده توضیح داده شده است، بازیابی کند.
  2. نمونه SnapshotContents را از طریق SnapshotsClient.SnapshotConflict بازیابی کنید.
  3. برای خواندن محتویات snapshot، تابع 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 اسنپ‌شات در بازی خود، این امکان وجود دارد که چندین دستگاه روی یک بازی ذخیره شده، عملیات خواندن و نوشتن را انجام دهند. در صورتی که یک دستگاه به طور موقت اتصال شبکه خود را از دست بدهد و بعداً دوباره متصل شود، این ممکن است باعث تداخل داده‌ها شود که در آن بازی ذخیره شده در دستگاه محلی بازیکن با نسخه راه دور ذخیره شده در سرورهای گوگل، همگام‌سازی نمی‌شود.

API مربوط به snapshots یک مکانیزم حل اختلاف ارائه می‌دهد که هر دو مجموعه از بازی‌های ذخیره شده‌ی دارای اختلاف را در زمان خواندن نمایش می‌دهد و به شما امکان می‌دهد یک استراتژی حل اختلاف مناسب برای بازی خود پیاده‌سازی کنید.

وقتی سرویس‌های بازی‌های گوگل پلی یک تداخل داده‌ای را تشخیص می‌دهند، متد SnapshotsClient.DataOrConflict.isConflict() مقدار true را برمی‌گرداند. در این رویداد، کلاس SnapshotsClient.SnapshotConflict دو نسخه از بازی ذخیره شده را ارائه می‌دهد:

  • نسخه سرور : جدیدترین نسخه‌ای که توسط سرویس‌های بازی‌های گوگل پلی برای دستگاه بازیکن دقیق شناخته شده است؛ و
  • نسخه محلی : نسخه اصلاح‌شده‌ای که در یکی از دستگاه‌های پخش‌کننده شناسایی شده و حاوی محتوا یا فراداده‌های متناقض است. این ممکن است با نسخه‌ای که سعی در ذخیره آن داشته‌اید، یکسان نباشد.

بازی شما باید با انتخاب یکی از نسخه‌های ارائه شده یا ادغام داده‌های دو نسخه ذخیره شده بازی، تصمیم بگیرد که چگونه این تداخل را حل کند.

برای تشخیص و رفع تداخل‌های بازی ذخیره شده:

  1. فراخوانی SnapshotsClient.open() نتیجه‌ی وظیفه شامل کلاس SnapshotsClient.DataOrConflict است.
  2. متد SnapshotsClient.DataOrConflict.isConflict() را فراخوانی کنید. اگر نتیجه true باشد، شما یک تداخل (conflict) برای حل کردن دارید.
  3. برای بازیابی یک نمونه از SnapshotsClient.snapshotConflict، تابع SnapshotsClient.snapshotConflict SnapshotsClient.DataOrConflict.getConflict() را فراخوانی کنید.
  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. برای دریافت یک شیء SnapshotContents جدید، تابع SnapshotsClient.SnapshotConflict.getResolutionSnapshotsContent() را فراخوانی کنید.
  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 را برمی‌گرداند. در این حالت، بازی شما باید به مرحله ۲ برگردد و مراحل اصلاح snapshot را تا زمان رفع تداخل‌ها تکرار کند.
    • اگر هیچ تداخلی وجود نداشته باشد، SnapshotsClient.DataOrConflict.isConflict() false را برمی‌گرداند و شیء Snapshot برای تغییر در بازی شما باز است.