يوضّح لك هذا الدليل كيفية تنفيذ ميزة "حفظ التقدم في الألعاب" باستخدام واجهة برمجة التطبيقات Snapshots API
التي تقدّمها "خدمات ألعاب Google Play". يمكن العثور على واجهات برمجة التطبيقات في الحِزم
com.google.android.gms.games.snapshot
و
com.google.android.gms.games
.
قبل البدء
للحصول على معلومات عن هذه الميزة، يُرجى الاطّلاع على نظرة عامة على "ألعاب محفوظة".
- فعِّل ميزة حفظ الألعاب للعبتك في Google Play Console.
- تنزيل نموذج رمز الألعاب المحفوظة ومراجعته في صفحة نماذج Android
- اطّلِع على الاقتراحات الموضّحة في قائمة التحقّق من الجودة.
الحصول على عميل اللقطات
لبدء استخدام واجهة برمجة التطبيقات 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 واجهة مستخدم تلقائية لاختيار الألعاب المحفوظة يمكنك استخدامها مباشرةً. تسمح واجهة مستخدِم اختيار الألعاب المحفوظة للاعبين بإنشاء لعبة محفوظة جديدة وعرض تفاصيل عن الألعاب المحفوظة الحالية وتحميل الألعاب المحفوظة السابقة.
لتشغيل واجهة المستخدم التلقائية لميزة "الألعاب المحفوظة":
- اتصل برقم
SnapshotsClient.getSelectSnapshotIntent()
لتلقّي رمز التفعيلIntent
لتشغيل واجهة المستخدم التلقائية لاختيار الألعاب المحفوظة. - اتصل بـ
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 // ... } } }
كتابة الألعاب المحفوظة
لتخزين المحتوى في لعبة محفوظة:
افتح لقطة شاشة بشكل غير متزامن باستخدام
SnapshotsClient.open()
.استرداد عنصر
Snapshot
من نتيجة المهمة من خلال استدعاءSnapshotsClient.DataOrConflict.getData()
استرداد
SnapshotContents
مثيل باستخدامSnapshotsClient.SnapshotConflict
استخدِم الدالة
SnapshotContents.writeBytes()
لتخزين بيانات اللاعب بتنسيق وحدات البايت.بعد كتابة كل التغييرات، اتصل بالرقم
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.
تحميل الألعاب المحفوظة
لاسترداد الألعاب المحفوظة الخاصة باللاعب الذي سجّل الدخول حاليًا:
فتح لقطة ضوئية بشكل غير متزامن باستخدام
SnapshotsClient.open()
استرجع عنصر
Snapshot
من نتيجة المهمة من خلال استدعاءSnapshotsClient.DataOrConflict.getData()
. بدلاً من ذلك، يمكن لتطبيقك أيضًا استرداد لقطة شاشة معيّنة من خلال واجهة مستخدم اختيار الألعاب المحفوظة، كما هو موضّح في مقالة عرض الألعاب المحفوظة.استردّ مثيل
SnapshotContents
باستخدامSnapshotsClient.SnapshotConflict
.اتصل بـ
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" ليكون دقيقًا لجهاز اللاعب
النسخة على الجهاز: نسخة معدَّلة تم رصدها على أحد أجهزة المشغّل وتتضمّن محتوى أو بيانات وصفية متضاربة. وقد لا يكون هذا الإصدار هو نفسه الإصدار الذي حاولت حفظه.
على لعبتك تحديد كيفية حلّ التعارض من خلال اختيار أحد الإصدارين المتوفرين أو دمج بيانات نسختَي اللعبة المحفوظتَين.
لرصد التعارضات في الألعاب المحفوظة وحلّها:
يُرجى الاتصال بالرقم
SnapshotsClient.open()
. تحتوي نتيجة المهمة على فئةSnapshotsClient.DataOrConflict
.استخدِم الطريقة
SnapshotsClient.DataOrConflict.isConflict()
. إذا كانت النتيجة true، يعني ذلك أنّ لديك تعارضًا يجب حلّه.اتصل بـ
SnapshotsClient.DataOrConflict.getConflict()
لاسترداد مثيلSnapshotsClient.snapshotConflict
.اتصل بالرقم
SnapshotsClient.SnapshotConflict.getConflictId()
لاسترداد معرّف التعارض الذي يحدّد التعارض الذي تم رصده بشكل فريد. تحتاج لعبتك إلى هذه القيمة لإرسال طلب لحلّ التعارض لاحقًا.يُرجى الاتصال برقم
SnapshotsClient.SnapshotConflict.getConflictingSnapshot()
للحصول على النسخة المحلية.يُرجى الاتصال برقم
SnapshotsClient.SnapshotConflict.getSnapshot()
للحصول على إصدار الخادم.لحلّ تعارض اللعبة المحفوظة، اختَر إصدارًا تريد حفظه على الخادم كإصدار نهائي، ثم أعِد توجيهه إلى الأسلوب
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
حالي لحفظه على الخادم كنسخة نهائية تم حلّها، اتّبِع الخطوات التالية:
يُرجى الاتصال بالرقم
SnapshotsClient.open()
.اتصل بـ
SnapshotsClient.SnapshotConflict.getResolutionSnapshotsContent()
للحصول على عنصرSnapshotContents
جديد.دمج البيانات من
SnapshotsClient.SnapshotConflict.getConflictingSnapshot()
وSnapshotsClient.SnapshotConflict.getSnapshot()
في كائنSnapshotContents
من الخطوة السابقةيمكنك اختياريًا إنشاء مثيل
SnapshotMetadataChange
في حال حدث أي تغييرات على حقول البيانات الوصفية.يُرجى الاتصال بالرقم
SnapshotsClient.resolveConflict()
. في طلب الطريقة، نقْلSnapshotsClient.SnapshotConflict.getConflictId()
كوسيطة أولى، والعنصرَينSnapshotMetadataChange
وSnapshotContents
اللذين عدّلتهما سابقًا كوسيطتَين ثانيتَين على التوالي.إذا تمت عملية الربط
SnapshotsClient.resolveConflict()
بنجاح، تخزِّن واجهة برمجة التطبيقات عنصرSnapshot
على الخادم ومحاولة فتح عنصر Snapshot على جهازك.- في حال تعارض القيم، تعرِض القيمة
SnapshotsClient.DataOrConflict.isConflict()
true
. في هذه الحالة، من المفترض أن تعود لعبتك إلى الخطوة 2 و تكرّر الخطوات لتعديل اللقطة إلى أن يتم حلّ التعارضات. - في حال عدم حدوث تعارض، يعرض العنصر
SnapshotsClient.DataOrConflict.isConflict()
false
ويكون العنصرSnapshot
متاحًا لتعديله في لعبتك.
- في حال تعارض القيم، تعرِض القيمة