תמיכה במשחקים שמורים ב-Android Games

במדריך הזה מוסבר איך להטמיע משחקים שמורים באמצעות ה-API של תמונות מצב, המסופקות על ידי שירותי המשחקים של Google Play. ממשקי ה-API נמצאים בחבילות com.google.android.gms.games.snapshot ו-com.google.android.gms.games.

לפני שמתחילים

אם עדיין לא עשיתם זאת, כדאי לעיין בקונספטים של משחקים שמורים.

קבלת לקוח של snapshots

כדי להתחיל להשתמש ב-Snapshots API, המשחק צריך תחילה לצבור אובייקט SnapshotsClient. תוכלו לעשות זאת על ידי קריאה לשיטה Games.getSnapshotsClient() והעברת הפעילות.

ציון ההיקף ב-Drive

ה-API של תמונות מצב מסתמך על Google Drive API לאחסון משחקים שמורים. כדי לקבל גישה ל-Drive API, כשאתם יוצרים את חשבון הכניסה של Google לאפליקציה, עליכם לציין את ההיקף של Drive.SCOPE_APPFOLDER.

הנה דוגמה שמראה איך לעשות זאת בשיטה 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 בכל מקום שבו המשחק מספק לשחקנים אפשרות לשמור או לשחזר את ההתקדמות שלהם. המשחק יכול להציג אפשרות כזו בנקודות שמירה/שחזור ייעודיות, או לאפשר לשחקנים לשמור או לשחזר את ההתקדמות בכל שלב.

אחרי שהשחקנים בוחרים באפשרות השמירה/שחזור במשחק שלכם, אתם יכולים (אם רוצים) להעלות מסך שמנחה את השחקנים להזין מידע לגבי משחק שמור חדש או לבחור משחק שמור קיים לשחזור.

כדי לפשט את תהליך הפיתוח, ה-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(). המשחק שלך יכול לבטל את הקריאה החוזרת (callback) הזו כדי לבדוק אם התרחשו שגיאות במהלך הבקשה.

קטע הקוד הבא מציג הטמעה לדוגמה של 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(), אפליקציית Play Games Services שומרת את נתוני המשחק השמורים באופן מקומי במכשיר. לאחר החיבור מחדש של המכשיר, שירותי המשחקים של 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.
          // ...
        }
      });
}

טיפול בהתנגשויות של משחקים שמורים

במהלך השימוש ב-Snapshot API של המשחק, מספר מכשירים יכולים לבצע קריאה וכתיבה באותו משחק. אם מכשיר מסוים מאבד את חיבור הרשת שלו באופן זמני ולאחר מכן מתחבר מחדש, הדבר עלול לגרום להתנגשויות נתונים שבהן המשחק השמור במכשיר המקומי של השחקן אינו מסונכרן עם הגרסה המרוחקת המאוחסנת בשרתי Google.

ה-API של תמונות המצב מספק מנגנון לפתרון התנגשויות, שמציג את שתי הקבוצות של משחקים מתנגשים בזמן אמת ומאפשר להשתמש באסטרטגיה מתאימה לפתרון המשחק.

כששירותי המשחקים של Google Play מזהים סתירה בנתונים, השיטה SnapshotsClient.DataOrConflict.isConflict() מחזירה את הערך true באירוע הזה, במחלקה SnapshotsClient.SnapshotConflict יש שתי גרסאות של המשחק שנשמר:

  • גרסת השרת: הגרסה העדכנית ביותר המוכרת על ידי שירותי המשחקים של Google Play, מדויקת ככל האפשר למכשיר של השחקן.
  • גרסה מקומית: גרסה שונה שזוהתה באחד מהמכשירים של הנגן ומכילה תוכן או מטא-נתונים מתנגשים. יכול להיות שהגרסה הזו לא זהה לגרסה שניסית לשמור.

המשחק שלכם צריך להחליט כיצד לפתור את ההתנגשות על ידי בחירה באחת משתי הגרסאות שסופקו או במיזוג הנתונים של שתי גרסאות המשחק שנשמרו.

כדי לזהות ולפתור התנגשויות במשחק שנשמרו:

  1. התקשרות אל SnapshotsClient.open(). התוצאה של המשימה מכילה כיתה אחת (SnapshotsClient.DataOrConflict).
  2. כדאי להפעיל את השיטה SnapshotsClient.DataOrConflict.isConflict(). אם התוצאה היא שקרית, אתם נתקלים בהתנגשות.
  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.
  5. התקשרות אל SnapshotsClient.resolveConflict(). בקריאה לשיטה, מעבירים את הארגומנט SnapshotsClient.SnapshotConflict.getConflictId() כארגומנט הראשון, ואת האובייקטים SnapshotMetadataChange ו-SnapshotContents ששינתם מוקדם יותר כארגומנטים השני והשלישי, בהתאמה.
  6. אם הקריאה אל SnapshotsClient.resolveConflict() הצליחה, ה-API מאחסן את האובייקט Snapshot לשרת ומנסה לפתוח את האובייקט של Snapshot במכשיר המקומי.