دعم خدمة "حفظ التقدم في الألعاب" في ألعاب Android

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

قبل البدء

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

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

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

تحديد نطاق Drive

تعتمد واجهة برمجة التطبيقات Snapshots API على Google Drive API لتخزين الألعاب المحفوظة. إلى الوصول إلى Drive API، يجب أن يحدّد تطبيقك 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
          }
        }
      });
}

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

يمكنك دمج واجهة برمجة تطبيقات لقطات الشاشة في أي مكان توفّر فيه لعبتك للّاعبين خيار حفظ مستوى تقدمه أو استعادته. قد تعرض لعبتك خيار حفظ نقاطك أو استعادتها، أو السماح للّاعبين بحفظ الملفات أو استعادتها التقدم في أي وقت.

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

لتبسيط عملية التطوير، توفّر واجهة Lookers API مستخدمًا تلقائيًا لاختيار الألعاب المحفوظة واجهة المستخدم (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.
          // ...
        }
      });
}

التعامل مع تعارضات الألعاب المحفوظة

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

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

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

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

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

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

  1. يُرجى الاتصال على SnapshotsClient.open(). تحتوي نتيجة المهمة على فئة SnapshotsClient.DataOrConflict.
  2. استخدِم الطريقة SnapshotsClient.DataOrConflict.isConflict(). إذا كانت النتيجة true، يعني ذلك أنّ لديك تعارضًا يجب حلّه.
  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 في حال حدوث أي تغييرات على حقول metadata .
  5. يُرجى الاتصال على SnapshotsClient.resolveConflict(). في استدعاء الطريقة، أدخِل SnapshotsClient.SnapshotConflict.getConflictId() كوسيطة أولى، و العنصران SnapshotMetadataChange وSnapshotContents اللذين عدّلتهما سابقًا في العنصر الثاني الوسيطات الثالثة على التوالي.
  6. إذا تم إكمال طلب SnapshotsClient.resolveConflict()، تخزِّن واجهة برمجة التطبيقات عنصر Snapshot على الخادم وتحاول فتح عنصر "الملخّص" على جهازك.
    • في حال حدوث تعارض، تعرِض الدالة SnapshotsClient.DataOrConflict.isConflict() القيمة true. في هذه الدورة، في هذه الحالة، يجب أن تعود لعبتك إلى الخطوة 2 وتكرر الخطوات لتعديل اللقطة حتى يتم حل النزاعات.
    • في حال عدم حدوث تعارض، يعرض SnapshotsClient.DataOrConflict.isConflict() القيمة false ويكون كائن Snapshot مفتوحًا لتعديله في لعبتك.