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

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

يوضّح لك هذا الدليل كيفية تنفيذ ميزة "حفظ التقدم في الألعاب" باستخدام واجهة برمجة التطبيقات الخاصة باللقطات التي توفّرها "خدمات ألعاب 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
          }
        }
      });
}

عرض بيانات &quot;حفظ التقدم في الألعاب&quot;

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

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

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

لإطلاق واجهة المستخدم التلقائية لـ &quot;حفظ التقدم في الألعاب&quot;، اتّبِع الخطوات التالية:

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

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

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

عندما ترصد &quot;خدمات ألعاب Google Play&quot; تعارضًا في البيانات، تعرض الطريقة 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 متاحًا للعبتك لتعديله.