دعم الألعاب المحفوظة في ألعاب Android

يوضِّح لك هذا الدليل كيفية تنفيذ لعبة ألعاب محفوظة باستخدام واجهة برمجة تطبيقات اللقطات التي تقدمها خدمات ألعاب Google Play. يمكن العثور على واجهات برمجة التطبيقات في حِزم com.google.android.gms.games.snapshot وcom.google.android.gms.games.

قبل البدء

إذا لم تكن قد فعلت ذلك من قبل، قد يكون من المفيد مراجعة مفاهيم الألعاب المحفوظة.

الحصول على برنامج "اللقطات"

لبدء استخدام Snapshot API، يجب أن تحصل اللعبة أولاً على كائن SnapshotsClient. ويمكنك إجراء ذلك من خلال استدعاء طريقة Games.getSnapshotsClient() وتمرير النشاط.

تحديد نطاق Drive

تعتمد واجهة برمجة تطبيقات Snapshots على واجهة برمجة تطبيقات Google Drive لتخزين الألعاب المحفوظة. للوصول إلى واجهة برمجة تطبيقات Drive، يجب أن يحدد تطبيقك نطاق Drive.SCOPE_APPFOLDER عند إنشاء برنامج تسجيل الدخول بحساب Google.

في ما يلي مثال على كيفية إجراء ذلك في طريقة 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 في أي مكان توفر فيه لعبتك للاعبين خيار حفظ مستوى التقدم الذي أحرزوه أو استعادته. قد تعرض لعبتك هذا الخيار عند نقاط الحفظ/الاستعادة المحددة أو تسمح للاعبين بحفظ أو استعادة التقدم في أي وقت.

بعد أن يختار اللاعبون خيار الحفظ/الاستعادة في لعبتك، يمكن للعبة أن تُظهر شاشة اختياريًا تطلب من اللاعبين إدخال معلومات حول لعبة محفوظة جديدة أو تحديد لعبة محفوظة حاليًا لاستعادتها.

لتبسيط عملية التطوير، توفّر واجهة برمجة تطبيقات اللقطات واجهة مستخدم (UI) مُختارة تلقائيًا للألعاب يمكن استخدامها على الفور. تسمح واجهة مستخدم اختيار الألعاب المحفوظة للاعبين بإنشاء لعبة محفوظة جديدة وعرض تفاصيل حول الألعاب المحفوظة الحالية وتحميل الألعاب المحفوظة السابقة.

لتشغيل واجهة مستخدم الألعاب المحفوظة الافتراضية:

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

إذا اختار اللاعب إنشاء لعبة محفوظة جديدة أو تحميل لعبة محفوظة حاليًا، سترسل واجهة المستخدم طلبًا إلى خدمات ألعاب 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(). وبعد ذلك، يمكنك استرداد الكائن Snapshot من نتيجة المهمة من خلال استدعاء SnapshotsClient.DataOrConflict.getData().
  2. يمكنك استرداد مثيل SnapshotContents من خلال SnapshotsClient.SnapshotConflict.
  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 =
      PlayGames.getSnapshotsClient(this);

  // Commit the operation
  return snapshotsClient.commitAndClose(snapshot, metadataChange);
}

إذا لم يكن جهاز المشغّل متصلاً بشبكة عندما يتصل تطبيقك SnapshotsClient.commitAndClose()، ستخزّن "خدمات ألعاب Google Play" بيانات اللعبة المحفوظة على الجهاز. وعند إعادة الاتصال بالجهاز، تزامن خدمات ألعاب Google Play تغييرات اللعبة المحفوظة محليًا في ذاكرة التخزين المؤقت على خوادم Google.

جارٍ تحميل الألعاب المحفوظة

لاسترداد الألعاب المحفوظة للمشغل الذي تم تسجيل دخوله حاليًا:

  1. افتح لقطة بشكل غير متزامن عبر SnapshotsClient.open(). وبعد ذلك، يمكنك استرداد الكائن Snapshot من نتيجة المهمة من خلال استدعاء SnapshotsClient.DataOrConflict.getData(). أو يمكن للعبة أيضًا استرداد لقطة محددة من خلال واجهة مستخدم اختيار الألعاب المحفوظة، كما هو موضَّح في عرض الألعاب المحفوظة.
  2. يمكنك استرداد مثيل SnapshotContents من خلال SnapshotsClient.SnapshotConflict.
  3. ويمكنك الاتصال بالرقم 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 في اللعبة، يمكن لعدة أجهزة إجراء عمليات القراءة والكتابة على اللعبة نفسها المحفوظة. في حالة انقطاع اتصال أحد الأجهزة مؤقتًا بالشبكة وإعادة الاتصال به لاحقًا، فقد يتسبب هذا في حدوث تضارب في البيانات حيث تكون اللعبة المحفوظة المخزنة على الجهاز المحلي للاعب غير متزامنة مع الإصدار البعيد المُخزن في خوادم Google.

توفر واجهة برمجة تطبيقات اللقطات آلية لتسوية التعارض تعرض مجموعتَي الألعاب المحفوظة المتعارضة في وقت القراءة وتسمح لك بتنفيذ استراتيجية حلّ مناسبة للعبتك.

عندما تكتشف خدمات ألعاب Google Play تعارضًا في البيانات، تعرض طريقة SnapshotsClient.DataOrConflict.isConflict() قيمة true. في هذه الحالة، توفر الفئة SnapshotsClient.SnapshotConflict نسختين من اللعبة المحفوظة:

  • إصدار الخادم: هو الإصدار الأحدث المعروف باسم "خدمات ألعاب Google Play" ليكون دقيقًا لجهاز المشغّل.
  • الإصدار المحلي: تم اكتشاف إصدار معدل على أحد أجهزة المشغل التي تتضمن محتوى أو بيانات وصفية متعارضة. وقد لا يتطابق هذا الإصدار مع الإصدار الذي حاولت حفظه.

يجب أن تحدِّد اللعبة كيفية حل التعارض من خلال اختيار أحد الإصدارات المقدمة أو دمج بيانات إصداري اللعبة المحفوظين.

لاكتشاف تعارضات الألعاب المحفوظة وحلّها:

  1. اتصل برقم الهاتف SnapshotsClient.open(). تتضمن نتيجة المهمة صفًا واحدًا (SnapshotsClient.DataOrConflict).
  2. عليك استدعاء طريقة SnapshotsClient.DataOrConflict.isConflict(). إذا كانت النتيجة صحيحة، فهذا يعني حدوث تعارض يجب حلّه.
  3. يمكنك استدعاء SnapshotsClient.DataOrConflict.getConflict() لاسترداد مثيل SnaphotsClient.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() بنجاح، تخزِّن واجهة برمجة التطبيقات الكائن Snapshot على الخادم وتحاول فتح كائن Snapshot على جهازك المحلي.