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

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

قبل البدء

للحصول على معلومات عن هذه الميزة، يُرجى الاطّلاع على نظرة عامة على "ألعاب محفوظة".

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

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

تحديد نطاق محرك الأقراص

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

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

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

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

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

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

إذا اختار اللاعب إنشاء ملف جديد محفوظ للّعبة أو تحميل ملف محفوظ حالي، تُرسِل واجهة المستخدم طلبًا إلى "خدمات ألعاب Play". إذا تم قبول الطلب، تُرسِل "خدمات ألعاب 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().

  2. استرداد عنصر Snapshot من نتيجة المهمة من خلال استدعاء SnapshotsClient.DataOrConflict.getData()

  3. استرداد SnapshotContents مثيل باستخدام SnapshotsClient.SnapshotConflict

  4. استخدِم الدالة SnapshotContents.writeBytes() لتخزين بيانات اللاعب بتنسيق وحدات البايت.

  5. بعد كتابة كل التغييرات، اتصل بالرقم SnapshotsClient.commitAndClose() لإرسال التغييرات إلى خوادم 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()، تخزِّن "خدمات ألعاب Play" بيانات اللعبة المحفوظة على الجهاز. عند إعادة ربط الجهاز، تعمل "خدمات ألعاب Play" على مزامنة التغييرات المحفوظة في ذاكرة التخزين المؤقت على الجهاز مع خوادم Google.

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

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

  1. فتح لقطة ضوئية بشكل غير متزامن باستخدام SnapshotsClient.open()

  2. استرجع عنصر Snapshot من نتيجة المهمة من خلال استدعاء SnapshotsClient.DataOrConflict.getData(). بدلاً من ذلك، يمكن لتطبيقك أيضًا استرداد لقطة شاشة معيّنة من خلال واجهة مستخدم اختيار الألعاب المحفوظة، كما هو موضّح في مقالة عرض الألعاب المحفوظة.

  3. استردّ مثيل SnapshotContents باستخدام SnapshotsClient.SnapshotConflict.

  4. اتصل بـ 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 API في لعبتك، يمكن لعدة أجهزة تنفيذ عمليات القراءة والكتابة في اللعبة المحفوظة نفسها. في حال انقطاع اتصال أحد الأجهزة بالشبكة مؤقتًا ثم إعادة الاتصال لاحقًا، قد يؤدي ذلك إلى حدوث تعارضات في البيانات، ما يؤدي إلى عدم مزامنة اللعبة المحفوظة على جهاز اللاعب مع النسخة البعيدة المخزّنة على خوادم Google.

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

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

  • إصدار الخادم: أحدث إصدار معروف لخدمة "ألعاب Play" ليكون دقيقًا لجهاز اللاعب

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

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

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

  1. يُرجى الاتصال بالرقم SnapshotsClient.open(). تحتوي نتيجة المهمة على فئة SnapshotsClient.DataOrConflict.

  2. استخدِم الطريقة SnapshotsClient.DataOrConflict.isConflict(). إذا كانت النتيجة true، يعني ذلك أنّ لديك تعارضًا يجب حلّه.

  3. اتصل بـ SnapshotsClient.DataOrConflict.getConflict() لاسترداد مثيل SnapshotsClient.snapshotConflict.

  4. اتصل بالرقم SnapshotsClient.SnapshotConflict.getConflictId() لاسترداد معرّف التعارض الذي يحدّد التعارض الذي تم رصده بشكل فريد. تحتاج لعبتك إلى هذه القيمة لإرسال طلب لحلّ التعارض لاحقًا.

  5. يُرجى الاتصال برقم SnapshotsClient.SnapshotConflict.getConflictingSnapshot() للحصول على النسخة المحلية.

  6. يُرجى الاتصال برقم SnapshotsClient.SnapshotConflict.getSnapshot() للحصول على إصدار الخادم.

  7. لحلّ تعارض اللعبة المحفوظة، اختَر إصدارًا تريد حفظه على الخادم كإصدار نهائي، ثم أعِد توجيهه إلى الأسلوب SnapshotsClient.resolveConflict().

يعرض المقتطف التالي مثالاً على كيفية تعامل لعبتك مع تعارض في ملف saved game من خلال اختيار آخر نسخة تم تعديلها من ملف saved game كنسخة نهائية يتم حفظها:

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 على جهازك.

    • في حال تعارض القيم، تعرِض القيمة SnapshotsClient.DataOrConflict.isConflict() true. في هذه الحالة، من المفترض أن تعود لعبتك إلى الخطوة 2 و تكرّر الخطوات لتعديل اللقطة إلى أن يتم حلّ التعارضات.
    • في حال عدم حدوث تعارض، يعرض العنصر SnapshotsClient.DataOrConflict.isConflict() false ويكون العنصر Snapshot متاحًا لتعديله في لعبتك.