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

بعد إيقاف واجهة برمجة التطبيقات تسجيل الدخول باستخدام Google نهائيًا، سنزيل الإصدار v1 من حزمة تطوير البرامج (SDK) للألعاب في عام 2026. بعد فبراير 2025، لن يكون بإمكانك نشر الألعاب التي تم دمجها حديثًا مع الإصدار v1 من حزمة SDK للألعاب على Google Play. ننصحك باستخدام الإصدار v2 من حزمة SDK للألعاب بدلاً من ذلك.
مع أنّ التطبيقات الحالية التي تتضمّن عمليات الدمج السابقة للإصدار 1 من "خدمات ألعاب Google Play" ستستمر في العمل لعدّة سنوات، ننصحك بنقل البيانات إلى الإصدار 2 بدءًا من يونيو 2025.
هذا الدليل مخصّص لاستخدام الإصدار 1 من حزمة تطوير البرامج (SDK) الخاصة بـ "خدمات ألعاب Play". للحصول على معلومات حول أحدث إصدار من حزمة SDK، يُرجى الاطّلاع على مستندات الإصدار 2.

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

قبل البدء

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

الحصول على عميل اللقطات

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

تحديد نطاق Drive

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

في ما يلي مثال على كيفية إجراء ذلك في طريقة 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
          }
        }
      });
}

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

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

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

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

لتشغيل واجهة المستخدم التلقائية لخدمة "حفظ التقدم في الألعاب"، اتّبِع الخطوات التالية:

  1. اتّصِل بالرقم SnapshotsClient.getSelectSnapshotIntent() للحصول على Intent لتشغيل واجهة المستخدم التلقائية لاختيار الألعاب المحفوظة.
  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);
    }
  });
}

إذا اختار اللاعب إنشاء لعبة محفوظة جديدة أو تحميل لعبة محفوظة حالية، ترسل واجهة المستخدم طلبًا إلى "خدمات ألعاب 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. في استدعاء الطريقة، يمكن أن تقدّم لعبتك اختياريًا معلومات إضافية لإخبار &quot;خدمات ألعاب Google Play&quot; بكيفية عرض هذه اللعبة المحفوظة للاعبين. يتم تمثيل هذه المعلومات في عنصر 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()، تخزّن "خدمات ألعاب Google Play" بيانات اللعبة المحفوظة محليًا على الجهاز. عند إعادة ربط الجهاز، تعمل &quot;خدمات ألعاب Google Play&quot; على مزامنة التغييرات التي تم إجراؤها على اللعبة المحفوظة والمخزَّنة مؤقتًا على الجهاز مع خوادم 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 =
      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.
          // ...
        }
      });
}

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

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

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

عندما ترصد "خدمات ألعاب Google Play" تعارضًا في البيانات، تعرض الطريقة SnapshotsClient.DataOrConflict.isConflict() القيمة true. وفي هذه الحالة، يوفّر الصف SnapshotsClient.SnapshotConflict إصدارَين من اللعبة المحفوظة:

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

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

لرصد التعارضات في الألعاب المحفوظة وحلّها، اتّبِع الخطوات التالية:

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