遷移至 Play 遊戲服務第 2 版 (Java 或 Kotlin)

本文說明如何將現有遊戲從遊戲第 1 版 SDK 遷移至遊戲第 2 版 SDK

事前準備

您可以透過偏好的 IDE (例如 Android Studio) 遷移遊戲。遷移至遊戲第 2 版前,請完成下列步驟:

  • 下載並安裝 Android Studio
  • 遊戲必須使用遊戲服務第 1 版 SDK。
  • 您可以將遊戲升級為使用遊戲服務第 1 版 SDK,以com.google.android.gms:play-services-games:24.0.0。請勿升級至 com.google.android.gms:play-services-games:25.0.0,因為遊戲服務第 1 版 API 已移除。

更新依附元件

  1. 在模組的 build.gradle 檔案中,找出模組層級依附元件中的這一行。

    implementation "com.google.android.gms:play-services-games:+"

    替換成以下程式碼:

    implementation "com.google.android.gms:play-services-games-v2:version"

    version 替換為最新版遊戲 SDK

  2. 更新依附元件後,請務必完成本文中的所有步驟。

定義專案 ID

如要在應用程式中加入 Play 遊戲服務 SDK 專案 ID,請完成下列步驟:

  1. AndroidManifest.xml 檔案中,將下列 <meta-data> 元素和屬性新增至 <application> 元素:

    <manifest>
      <application>
        <meta-data android:name="com.google.android.gms.games.APP_ID"
                   android:value="@string/game_services_project_id"/>
      </application>
    </manifest>
    

    使用遊戲的遊戲服務專案 ID 為值,定義字串資源參考資料 @string/game_services_project_id。遊戲服務專案 ID 會在 Google Play 管理中心「設定」頁面的遊戲名稱下方顯示。

  2. res/values/strings.xml 檔案中,新增字串資源參考資料,並將專案 ID 設為該值。例如:

    <!-- res/values/strings.xml -->
    <resources>
      <!-- Replace 0000000000 with your game’s project id. Example value shown above.  -->
      <string translatable="false"  name="game_services_project_id"> 0000000000 </string>
    </resources>
    

遷移路徑

遊戲的正確遷移路徑取決於如何實作 Play Games 服務第 1 版,以及如何處理玩家身分。為確保順利轉換並避免遺失玩家資料,請找出最符合現有設定的情境,然後按照對應步驟操作。

做法 1:針對 IGA 繫結至 Play 遊戲服務玩家 ID 的遊戲

這個情境適用於遊戲,這些遊戲使用 Play 遊戲服務 Player ID做為玩家遊戲內帳戶 (IGA) 的唯一 ID,且先前未要求或儲存 OpenID。主要挑戰是將現有 IGA 連結至主要 ID (OpenID),同時保留與玩家進度的連結。

遷移流程包含下列步驟:

  1. 遊戲推出時,Play Games 服務第 2 版 SDK 會自動且無聲地驗證平台。
  2. 顯示登入畫面,其中包含「使用 Google 帳戶登入」按鈕,取代「Google Play」按鈕。例如,請參閱 CredManBridge.java

    CredManBridge.java
    
    package com.wickedcube.trivialkart;
    import android.accounts.Account;
    import android.content.Context;
    import android.util.Log;
    import android.os.CancellationSignal;
    import androidx.credentials.CredentialManager;
    import androidx.credentials.GetCredentialRequest;
    import androidx.credentials.GetCredentialResponse;
    import androidx.credentials.exceptions.GetCredentialException;
    import androidx.credentials.exceptions.NoCredentialException;
    import com.google.android.libraries.identity.googleid.GetGoogleIdOption;
    import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential;
    import com.google.android.gms.auth.api.identity.AuthorizationClient;
    import com.google.android.gms.auth.api.identity.AuthorizationRequest;
    import com.google.android.gms.auth.api.identity.AuthorizationResult;
    import com.google.android.gms.common.api.ApiException;
    import com.google.android.gms.auth.api.identity.Identity;
    import com.google.android.gms.common.api.Scope;
    import com.unity3d.player.UnityPlayer;
    import java.util.Collections;
    import java.util.List;
    import java.util.concurrent.Executor;
    import java.util.concurrent.Executors;

    public class CredManBridge {

    // --- MODE 1: SILENT SIGN-IN (Called on Awake) --- // Tries to auto-select an authorized account. If it fails, it does NOT show UI. public static void signInSilent(Context context, String webClientId) { CredentialManager credentialManager = CredentialManager.create(context); CancellationSignal cancellationSignal = new CancellationSignal(); Executor executor = Executors.newSingleThreadExecutor();

    Log.d("CredMan", "Attempting Silent Sign-In...");
    
    GetGoogleIdOption silentOption = new GetGoogleIdOption.Builder()
        .setFilterByAuthorizedAccounts(true) // Strict: Only authorized accounts
        .setServerClientId(webClientId)
        .setAutoSelectEnabled(true)          // Auto-select if possible
        .build();
    
    GetCredentialRequest silentRequest = new GetCredentialRequest.Builder()
        .addCredentialOption(silentOption)
        .build();
    
    credentialManager.getCredentialAsync(
        context,
        silentRequest,
        cancellationSignal,
        executor,
        new androidx.credentials.CredentialManagerCallback<GetCredentialResponse, GetCredentialException>() {
            @Override
            public void onResult(GetCredentialResponse result) {
                Log.d("CredMan", "Silent Sign-In Successful!");
                handleSignInResult(context, result, webClientId);
            }
    
            @Override
            public void onError(GetCredentialException e) {
                // Send a specific error code so Unity knows to just stay on the Start Screen
                Log.d("CredMan", "Silent sign-in failed. Keeping UI hidden.");
                UnityPlayer.UnitySendMessage("AuthManager", "OnSignInError", "SilentFailed");
            }
        }
    );
    

    }

    // --- MODE 2: INTERACTIVE SIGN-IN (Called on Button Click) --- // Forces the Account Selection / "Add Account" sheet to appear. public static void signInInteractive(Context context, String webClientId) { CredentialManager credentialManager = CredentialManager.create(context); CancellationSignal cancellationSignal = new CancellationSignal(); Executor executor = Executors.newSingleThreadExecutor();

    Log.d("CredMan", "Starting Interactive Sign-In...");
    
    GetGoogleIdOption interactiveOption = new GetGoogleIdOption.Builder()
        .setFilterByAuthorizedAccounts(false) // Show ALL accounts (and "Add Account")
        .setServerClientId(webClientId)
        .setAutoSelectEnabled(false)          // Force the UI to show
        .build();
    
    GetCredentialRequest interactiveRequest = new GetCredentialRequest.Builder()
        .addCredentialOption(interactiveOption)
        .build();
    
    credentialManager.getCredentialAsync(
        context,
        interactiveRequest,
        cancellationSignal,
        executor,
        new androidx.credentials.CredentialManagerCallback<GetCredentialResponse, GetCredentialException>() {
            @Override
            public void onResult(GetCredentialResponse result) {
                Log.d("CredMan", "Interactive Sign-In Successful!");
                handleSignInResult(context, result, webClientId);
            }
    
            @Override
            public void onError(GetCredentialException e) {
                Log.e("CredMan", "Interactive Sign-In Canceled or Failed", e);
                UnityPlayer.UnitySendMessage("AuthManager", "OnSignInError", "Canceled");
            }
        }
    );
    

    }

    private static void handleSignInResult(Context context, GetCredentialResponse result, String webClientId) { try { GoogleIdTokenCredential credential = GoogleIdTokenCredential.createFrom(result.getCredential().getData()); String email = credential.getId();

        Account account = new Account(email, "com.google");
        // Requesting GAMES_LITE scope to check for pre-existing V1 grants
        List<Scope> requestedScopes = Collections.singletonList(new Scope("https://www.googleapis.com/auth/games_lite"));
    
        AuthorizationRequest authRequest = new AuthorizationRequest.Builder()
            .setRequestedScopes(requestedScopes)
            .setAccount(account)
            .requestOfflineAccess(webClientId)
            .build();
    
        AuthorizationClient authClient = Identity.getAuthorizationClient(context);
    
        authClient.authorize(authRequest)
            .addOnSuccessListener(authorizationResult -> {
                if (authorizationResult.getServerAuthCode() != null) {
                    // CASE 1: RETURNING USER (Success)
                    // The user has already granted GAMES_LITE in the past.
                    // We got the code directly without showing UI.
                    Log.i("CredMan", "PGS v1: Existing grant found. Returning user detected. Auth Code retrieved.");
                    UnityPlayer.UnitySendMessage("AuthManager", "OnSignInSuccess", authorizationResult.getServerAuthCode());
                }
                else if (authorizationResult.hasResolution()) {
                    // CASE 2: NEW USER (PendingIntent)
                    // The user has NOT granted GAMES_LITE before. The API returned a PendingIntent
                    // (authorizationResult.getPendingIntent()) to show the consent screen.
                    // As per your flow, we DISCARD this intent and do not show UI.
                    Log.i("CredMan", "PGS v1: No existing grant (PendingIntent returned). This is a NEW user or they revoked access.");
                    Log.i("CredMan", "PGS v1: Discarding PendingIntent. Proceeding as New User.");
    
                    // Notify Unity that this is a "New User" so it can trigger V2 logic instead of failing
                    UnityPlayer.UnitySendMessage("AuthManager", "OnSignInError", "NewUser_NoGrant");
                }
                else {
                    // Edge Case: No code and no resolution?
                    Log.e("CredMan", "PGS v1: Authorization success but no Auth Code or Resolution returned.");
                    UnityPlayer.UnitySendMessage("AuthManager", "OnSignInError", "No Auth Code returned");
                }
            })
            .addOnFailureListener(e -> {
                // CASE 3: GENERIC FAILURE
                Log.e("CredMan", "PGS v1: Authorization failed completely.", e);
                UnityPlayer.UnitySendMessage("AuthManager", "OnSignInError", "Authorization Failed: " + e.getMessage());
            });
    
    } catch (Exception e) {
        UnityPlayer.UnitySendMessage("AuthManager", "OnSignInError", "Parsing Error: " + e.getMessage());
    }
    

    } }

  3. 當玩家輕觸「使用 Google 帳戶登入」按鈕並選取 Google 帳戶時,請擷取兩個不同的 ID:

    • OpenID,這是繫結 IGA 的主要 ID。
    • 使用 GAMES_LITE 範圍擷取的 Play 遊戲服務 Player ID,可在後端系統中查詢玩家的 IGA 並執行繫結。詳情請參閱「擷取 Player ID」。
  4. 在後續啟動遊戲時,透過「使用 Google 帳戶登入」流程存取 IGA,遊戲不必使用 Player ID 做為主要 ID。

擷取 Player ID

您可以使用遊戲用戶端實作項目執行步驟 3。

  1. 呼叫 Android Credential Manager API,讓使用者透過 Google 帳戶登入。
  2. 使用者完成「使用 Google 帳戶登入」流程並選取 Google 帳戶後,您會收到包含 ID 權杖和電子郵件地址的結果物件。
  3. 從電子郵件地址建構 Account 物件。
  4. 使用 GAMES_LITE 範圍和帳戶呼叫 Authorization API。
  5. 如果帳戶在 GAMES_LITE 範圍內已有授權,Authorization API 會直接在回應物件中傳回權杖:
    1. 使用回應權杖呼叫 Play Games 服務伺服器,並擷取 Play Games 服務 Player ID
    2. 確認 Play 遊戲服務 Player ID 是否已連結遊戲內帳戶。
      1. 這表示使用者是從 Play 遊戲服務第 1 版回訪。
    3. 將新的 Gaia ID 連結至先前的 Play Games 服務第 1 版帳戶。
  6. 如果帳戶在 GAMES_LITE 範圍內沒有預先存在的授權,Authorization API 會傳回 PendingIntent
    1. 這表示使用者沒有 Play Games 服務第 1 版的現有帳戶。
    2. 安全捨棄 PendingIntent,且不顯示任何 UI。

方法 2:遊戲已將 IGA 繫結至 OpenID

這個群組的開發人員遷移路徑最簡單。如果遊戲內帳戶已主要繫結至 OpenID,您只需要按照步驟,從第 1 版執行標準技術 SDK 遷移至第 2 版。

從已淘汰的 Google 登入服務遷移

GoogleSignInClient 類別替換為 GamesSignInClient 類別。

Java

找出 GoogleSignInClient 類別的檔案。

import com.google.android.gms.auth.api.signin.GoogleSignIn;
import com.google.android.gms.auth.api.signin.GoogleSignInClient;
import com.google.android.gms.auth.api.signin.GoogleSignInOptions;

// ... existing code

@Override
public void onCreate(@Nullable Bundle bundle) {
    super.onCreate(bundle);

    // ... existing code

    GoogleSignInOptions signInOption =
        new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_GAMES_SIGN_IN).build();
    
    // Client used to sign in to Google services
    GoogleSignInClient googleSignInClient =
        GoogleSignIn.getClient(this, signInOptions);
}

並更新為以下程式碼:

import com.google.android.gms.games.PlayGamesSdk;
import com.google.android.gms.games.PlayGames;
import com.google.android.gms.games.GamesSignInClient;

// ... existing code

@Override
public void onCreate(){
    super.onCreate();
    PlayGamesSdk.initialize(this);
    // Client used to sign in to Google services
    GamesSignInClient gamesSignInClient =
        PlayGames.getGamesSignInClient(getActivity());
}

Kotlin

找出 GoogleSignInClient 類別的檔案。

import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInClient
import com.google.android.gms.auth.api.signin.GoogleSignInOptions

// ... existing code

val signInOptions = GoogleSignInOptions.DEFAULT_GAMES_SIGN_IN

// ... existing code

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val googleSignInClient: GoogleSignInClient =
        GoogleSignIn.getClient(this, signInOptions)
}

並更新為以下程式碼:

import com.google.android.gms.games.PlayGames
import com.google.android.gms.games.PlayGamesSdk
import com.google.android.gms.games.GamesSignInClient

// ... existing code

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    PlayGamesSdk.initialize(this)
    // client used to sign in to Google services
    val gamesSignInClient: GamesSignInClient =
        PlayGames.getGamesSignInClient(this)
}

更新 GoogleSignIn 程式碼

遊戲服務第 2 版 SDK 不支援 GoogleSignIn API。請將 GoogleSignIn API 程式碼替換為 GamesSignInClient API,如下方範例所示。

如要要求伺服器端存取權杖,請使用 GamesSignInClient.requestServerSideAccess() 方法。詳情請參閱「更新伺服器端存取權類別」。

Java

找出 GoogleSignIn 類別的檔案。

// Request code used when invoking an external activity.
private static final int RC_SIGN_IN = 9001;

private boolean isSignedIn() {
    GoogleSignInAccount account = GoogleSignIn.getLastSignedInAccount(this);
    GoogleSignInOptions signInOptions =
    GoogleSignInOptions.DEFAULT_GAMES_SIGN_IN;
    return GoogleSignIn.hasPermissions(account, signInOptions.getScopeArray());
}

private void signInSilently() {
    GoogleSignInOptions signInOptions =
        GoogleSignInOptions.DEFAULT_GAMES_SIGN_IN;
    GoogleSignInClient signInClient = GoogleSignIn.getClient(this, signInOptions);
    signInClient
        .silentSignIn()
        .addOnCompleteListener(
            this,
            task -> {
            if (task.isSuccessful()) {
                // The signed-in account is stored in the task's result.
                GoogleSignInAccount signedInAccount = task.getResult();
                showSignInPopup();
            } else {
                // Perform interactive sign in.
                startSignInIntent();
            }
        });
}

private void startSignInIntent() {
    GoogleSignInClient signInClient = GoogleSignIn.getClient(this,
        GoogleSignInOptions.DEFAULT_GAMES_SIGN_IN);
    Intent intent = signInClient.getSignInIntent();
    startActivityForResult(intent, RC_SIGN_IN);
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == RC_SIGN_IN) {
        GoogleSignInResult result =
        Auth.GoogleSignInApi.getSignInResultFromIntent(data);
        if (result.isSuccess()) {
            // The signed-in account is stored in the result.
            GoogleSignInAccount signedInAccount = result.getSignInAccount();
            showSignInPopup();
        } else {
            String message = result.getStatus().getStatusMessage();
            if (message == null || message.isEmpty()) {
                message = getString(R.string.signin_other_error);
        }
        new AlertDialog.Builder(this).setMessage(message)
            .setNeutralButton(android.R.string.ok, null).show();
        }
    }
}

private void showSignInPopup() {
Games.getGamesClient(requireContext(), signedInAccount)
    .setViewForPopups(contentView)
    .addOnCompleteListener(
        task -> {
            if (task.isSuccessful()) {
                logger.atInfo().log("SignIn successful");
            } else {
                logger.atInfo().log("SignIn failed");
            }
        });
  }

並更新為以下程式碼:

private void signInSilently() {
    gamesSignInClient.isAuthenticated().addOnCompleteListener(isAuthenticatedTask -> {
    boolean isAuthenticated =
        (isAuthenticatedTask.isSuccessful() &&
            isAuthenticatedTask.getResult().isAuthenticated());
        if (isAuthenticated) {
            // Continue with Play Games Services
        } else {
            // If authentication fails, either disable Play Games Services
            // integration or
            // display a login button to prompt players to sign in.
            // Use`gamesSignInClient.signIn()` when the login button is clicked.
        }
    });
}

@Override
protected void onResume() {
    super.onResume();
    // When the activity is inactive, the signed-in user's state can change;
    // therefore, silently sign in when the app resumes.
    signInSilently();
}

Kotlin

找出 GoogleSignIn 類別的檔案。

// Request codes we use when invoking an external activity.
private val RC_SIGN_IN = 9001

// ... existing code

private fun isSignedIn(): Boolean {
    val account = GoogleSignIn.getLastSignedInAccount(this)
    val signInOptions = GoogleSignInOptions.DEFAULT_GAMES_SIGN_IN
    return GoogleSignIn.hasPermissions(account, *signInOptions.scopeArray)
}

private fun signInSilently() {
    val signInOptions = GoogleSignInOptions.DEFAULT_GAMES_SIGN_IN
    val signInClient = GoogleSignIn.getClient(this, signInOptions)
    signInClient.silentSignIn().addOnCompleteListener(this) { task ->
        if (task.isSuccessful) {
            // The signed-in account is stored in the task's result.
            val signedInAccount = task.result
            // Pass the account to showSignInPopup.
            showSignInPopup(signedInAccount)
        } else {
            // Perform interactive sign in.
            startSignInIntent()
        }
    }
}

private fun startSignInIntent() {
    val signInClient = GoogleSignIn.getClient(this, GoogleSignInOptions.DEFAULT_GAMES_SIGN_IN)
    val intent = signInClient.signInIntent
    startActivityForResult(intent, RC_SIGN_IN)
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (requestCode == RC_SIGN_IN) {
        val result = Auth.GoogleSignInApi.getSignInResultFromIntent(data)
        if (result.isSuccess) {
            // The signed-in account is stored in the result.
            val signedInAccount = result.signInAccount
            showSignInPopup(signedInAccount) // Pass the account to showSignInPopup.
        } else {
            var message = result.status.statusMessage
            if (message == null || message.isEmpty()) {
                message = getString(R.string.signin_other_error)
        }
        AlertDialog.Builder(this)
            .setMessage(message)
            .setNeutralButton(android.R.string.ok, null)
            .show()
        }
    }
}

private fun showSignInPopup(signedInAccount: GoogleSignInAccount) {
    // Add signedInAccount parameter.
    Games.getGamesClient(this, signedInAccount)
        .setViewForPopups(contentView) // Assuming contentView is defined.
        .addOnCompleteListener { task ->
        if (task.isSuccessful) {
            logger.atInfo().log("SignIn successful")
        } else {
            logger.atInfo().log("SignIn failed")
        }
    }
}

並更新為以下程式碼:

private fun signInSilently() {
    gamesSignInClient.isAuthenticated.addOnCompleteListener { isAuthenticatedTask ->
        val isAuthenticated = isAuthenticatedTask.isSuccessful &&
        isAuthenticatedTask.result.isAuthenticated
        if (isAuthenticated) {
            // Continue with Play Games Services
        } else {
            // To handle a user who is not signed in, either disable Play Games Services integration
            // or display a login button. Selecting this button calls `gamesSignInClient.signIn()`.
        }
    }
}

override fun onResume() {
    super.onResume()
    // Since the state of the signed in user can change when the activity is
    // not active it is recommended to try and sign in silently from when the
    // app resumes.
    signInSilently()
}

新增 GamesSignInClient 程式碼

如果玩家成功通過驗證,請從遊戲中移除 Play 遊戲服務登入按鈕。如果使用者選擇不要在遊戲啟動時驗證,請繼續顯示有 Play Games 服務圖示的按鈕,並使用 GamesSignInClient.signIn() 啟動登入程序。

Java

private void startSignInIntent() {
    gamesSignInClient
        .signIn()
        .addOnCompleteListener( task -> {
            if (task.isSuccessful() && task.getResult().isAuthenticated()) {
                // sign in successful
            } else {
                // sign in failed
            }
        });
  }

Kotlin

private fun startSignInIntent() {
    gamesSignInClient
        .signIn()
        .addOnCompleteListener { task ->
            if (task.isSuccessful && task.result.isAuthenticated) {
                // sign in successful
            } else {
                // sign in failed
            }
        }
  }

移除登出代碼

移除 GoogleSignInClient.signOut 的程式碼。

移除下列範例所示的程式碼:

Java

// ... existing code

private void signOut() {
    GoogleSignInClient signInClient = GoogleSignIn.getClient(this,
    GoogleSignInOptions.DEFAULT_GAMES_SIGN_IN);
    signInClient.signOut().addOnCompleteListener(this,
    new OnCompleteListener() {
        @Override
        public void onComplete(@NonNull Task task) {
           // At this point, the user is signed out.
        }
    });
}

Kotlin

// ... existing code

private fun signOut() {
    val signInClient = GoogleSignIn.getClient(this, GoogleSignInOptions.DEFAULT_GAMES_SIGN_IN)
    signInClient.signOut().addOnCompleteListener(this) {
    // At this point, the user is signed out.
    }
}

確認驗證是否成功

加入下列程式碼,檢查您是否已自動驗證,並新增自訂邏輯 (如有)。

Java

private void checkIfAutomaticallySignedIn() {
gamesSignInClient.isAuthenticated().addOnCompleteListener(isAuthenticatedTask -> {
boolean isAuthenticated =
    (isAuthenticatedTask.isSuccessful() &&
    isAuthenticatedTask.getResult().isAuthenticated());

    if (isAuthenticated) {
        // Continue with Play Games Services
        // If your game requires specific actions upon successful sign-in,
        // you can add your custom logic here.
        // For example, fetching player data or updating UI elements.
    } else {
        // Show a login button to ask  players to sign-in. Clicking it should
        // call GamesSignInClient.signIn().
        }
    });
}

Kotlin

private void checkIfAutomaticallySignedIn() {
gamesSignInClient.isAuthenticated()
    .addOnCompleteListener { task ->
    val isAuthenticated = task.isSuccessful && task.result?.isAuthenticated ?: false

        if (isAuthenticated) {
            // Continue with Play Games Services
        } else {
            // Disable your integration or show a login button
        }
    }
}

更新用戶端類別名稱和方法

遷移至遊戲服務第 2 版後,取得用戶端類別名稱的方法會有所不同。請改用對應的 PlayGames.getxxxClient() 方法,而非 Games.getxxxClient() 方法。

舉例來說,針對 LeaderboardsClient,請使用 PlayGames.getLeaderboardsClient(),而非 Games.getLeaderboardsClient() 方法。

移除與 GamesClientGamesMetadataClient 類別相關的任何程式碼,因為遊戲 v2 中沒有任何替代類別。

Java

找出 LeaderboardsClient 的程式碼。

import com.google.android.gms.games.LeaderboardsClient;
import com.google.android.gms.games.Games;

@Override
public void onCreate(@Nullable Bundle bundle) {
    super.onCreate(bundle);
        // Get the leaderboards client using Play Games services.
    LeaderboardsClient leaderboardsClient = Games.getLeaderboardsClient(this,
        GoogleSignIn.getLastSignedInAccount(this));
}

並更新為以下程式碼:

import com.google.android.gms.games.LeaderboardsClient;
import com.google.android.gms.games.PlayGames;

 @Override
public void onCreate(@Nullable Bundle bundle) {
    super.onCreate(bundle);
        // Get the leaderboards client using Play Games services.
        LeaderboardsClient leaderboardsClient = PlayGames.getLeaderboardsClient(getActivity());
}

Kotlin

找出 LeaderboardsClient 的程式碼。

import com.google.android.gms.games.LeaderboardsClient
import com.google.android.gms.games.Games
// Initialize the variables.
private lateinit var leaderboardsClient: LeaderboardsClient

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    leaderboardsClient = Games.getLeaderboardsClient(this,
        GoogleSignIn.getLastSignedInAccount(this))
}

並更新為以下程式碼:

import com.google.android.gms.games.LeaderboardsClient
import com.google.android.gms.games.PlayGames
    // Initialize the variables.
private lateinit var leaderboardsClient: LeaderboardsClient

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    leaderboardsClient = PlayGames.getLeaderboardsClient(this)
}

同樣地,請針對下列用戶端使用對應的方法:AchievementsClientEventsClientGamesSignInClientPlayerStatsClientRecallClientSnapshotsClientPlayersClient

更新伺服器端存取權類別

如要要求伺服器端存取權杖,請使用 GamesSignInClient.requestServerSideAccess() 方法,而非 GoogleSignInAccount.getServerAuthCode() 方法。

詳情請參閱「傳送伺服器授權碼」。

以下範例說明如何要求伺服器端存取權杖。

Java

找到 GoogleSignInOptions 類別的程式碼。

    private static final int RC_SIGN_IN = 9001;
    private GoogleSignInClient googleSignInClient;

    private void startSignInForAuthCode() {
        /** Client ID for your backend server. */
        String webClientId = getString(R.string.webclient_id);
        GoogleSignInOptions signInOption = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_GAMES_SIGN_IN)
            .requestServerAuthCode(webClientId)
            .build();

        GoogleSignInClient signInClient = GoogleSignIn.getClient(this, signInOption);
        Intent intent = signInClient.getSignInIntent();
        startActivityForResult(intent, RC_SIGN_IN);
    }

    /** Auth code to send to backend server */
    private String mServerAuthCode;

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == RC_SIGN_IN) {
            GoogleSignInResult result = Auth.GoogleSignInApi.getSignInResultFromIntent(data);
        if (result.isSuccess()) {
            mServerAuthCode = result.getSignInAccount().getServerAuthCode();
        } else {
            String message = result.getStatus().getStatusMessage();
            if (message == null || message.isEmpty()) {
                message = getString(R.string.signin_other_error);
            }
            new AlertDialog.Builder(this).setMessage(message)
                .setNeutralButton(android.R.string.ok, null).show();
        }
      }
    }
  

並更新為以下程式碼:

  private void startRequestServerSideAccess() {
      GamesSignInClient gamesSignInClient = PlayGames.getGamesSignInClient(this);
      gamesSignInClient
          .requestServerSideAccess(OAUTH_2_WEB_CLIENT_ID,
           /* forceRefreshToken= */ false, /* additional AuthScope */ scopes)
          .addOnCompleteListener(task -> {
              if (task.isSuccessful()) {
                  AuthResponse authresp = task.getResult();
                  // Send the authorization code as a string and a
                  // list of the granted AuthScopes that were granted by the
                  // user. Exchange for an access token.
                  // Verify the player with Play Games Services REST APIs.
              } else {
                // Authentication code retrieval failed.
              }
        });
  }
  

Kotlin

找到 GoogleSignInOptions 類別的程式碼。

  // ... existing code

  private val RC_SIGN_IN = 9001
  private lateinit var googleSignInClient: GoogleSignInClient

  // Auth code to send to backend server.
  private var mServerAuthCode: String? = null

  private fun startSignInForAuthCode() {
      // Client ID for your backend server.
      val webClientId = getString(R.string.webclient_id)

      val signInOption = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_GAMES_SIGN_IN)
          .requestServerAuthCode(webClientId)
          .build()

      googleSignInClient = GoogleSignIn.getClient(this, signInOption)
      val intent = googleSignInClient.signInIntent
      startActivityForResult(intent, RC_SIGN_IN)
  }

  override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
      super.onActivityResult(requestCode, resultCode, data)
      if (requestCode == RC_SIGN_IN) {
          val result = Auth.GoogleSignInApi.getSignInResultFromIntent(data)
          if (result.isSuccess) {
              mServerAuthCode = result.signInAccount.serverAuthCode
          } else {
              var message = result.status.statusMessage
              if (message == null || message.isEmpty()) {
                  message = getString(R.string.signin_other_error)
              }
              AlertDialog.Builder(this).setMessage(message)
                  .setNeutralButton(android.R.string.ok, null).show()
            }
        }
  }
  

並更新為以下程式碼:

  private void startRequestServerSideAccess() {
  GamesSignInClient gamesSignInClient = PlayGames.getGamesSignInClient(this);
      gamesSignInClient
          .requestServerSideAccess(OAUTH_2_WEB_CLIENT_ID, /* forceRefreshToken= */ false,
          /* additional AuthScope */ scopes)
          .addOnCompleteListener(task -> {
              if (task.isSuccessful()) {
                  AuthResponse authresp = task.getResult();
                  // Send the authorization code as a string and a
                  // list of the granted AuthScopes that were granted by the
                  // user. Exchange for an access token.
                  // Verify the player with Play Games Services REST APIs.
              } else {
                // Authentication code retrieval failed.
              }
        });
  }
  

從 GoogleApiClient 遷移

較舊的現有遊戲整合功能可能因 Play 遊戲服務 SDK 的 GoogleApiClient API 變化版本而異。此設定已於 2017 年年底淘汰,並由「無網路連線」的用戶端取代。如要遷移,可以使用對等的「無網路連線」取代 GoogleApiClient 類別。下表列出遊戲第 1 版到第 2 版的常見類別對應:

games v2 (目前) games v1 (舊版)
com.google.android.gms.games.AchievementsClient com.google.android.gms.games.achievement.Achievements
com.google.android.gms.games.LeaderboardsClient com.google.android.gms.games.leaderboard.Leaderboard
com.google.android.gms.games.SnapshotsClient com.google.android.gms.games.snapshot.Snapshots
com.google.android.gms.games.PlayerStatsClient com.google.android.gms.games.stats.PlayerStats
com.google.android.gms.games.PlayersClient com.google.android.gms.games.Players
com.google.android.gms.games.GamesClientStatusCodes com.google.android.gms.games.GamesStatusCodes

建構並執行遊戲

如要在 Android Studio 中建構及執行,請參閱「建構並執行應用程式」。

測試遊戲

請測試遊戲,確保遊戲功能符合設計。您執行的測試取決於遊戲功能。

以下列出常見的測試。

  1. 成功登入

    1. 自動登入功能正常運作。使用者啟動遊戲時,應登入 Play 遊戲服務。

    2. 系統會顯示歡迎彈出式視窗。

      歡迎訊息彈出式視窗範例。
      歡迎彈出式視窗範例 (按一下可放大)。

    3. 系統會顯示成功記錄訊息。在終端機中執行下列指令:

      adb logcat | grep com.google.android.

      以下範例顯示成功記錄訊息:

      [$PlaylogGamesSignInAction$SignInPerformerSource@e1cdecc
      number=1 name=GAMES_SERVICE_BROKER>], returning true for shouldShowWelcomePopup.
      [CONTEXT service_id=1 ]
  2. 確保 UI 元件一致性

    1. 在 Play 遊戲服務使用者介面 (UI) 中,彈出式視窗、排行榜和成就能在各種螢幕大小和方向下正確且一致地顯示。

    2. Play Games 服務使用者介面中沒有登出選項。

    3. 確認您能順利擷取玩家 ID,且伺服器端功能運作正常 (如適用)。

    4. 如果遊戲使用伺服器端驗證,請徹底測試 requestServerSideAccess 流程。確認伺服器收到驗證碼,並可交換存取權杖。測試網路錯誤的成功和失敗情境,以及無效的 client ID 情境。

如果遊戲使用下列任何功能,請測試這些功能,確保遷移後運作方式與遷移前相同:

  • 排行榜:提交分數並查看排行榜。確認玩家名稱和分數的排名與顯示方式是否正確。
  • 成就:解鎖成就,並確認成就已正確記錄及顯示在 Play Games 使用者介面中。
  • 遊戲進度存檔:如果遊戲使用遊戲進度存檔,請確保儲存和載入遊戲進度時不會發生任何問題。因此請務必在多部裝置上測試,並在應用程式更新後再次測試。

遷移後工作

遷移至 Games v2 後,請完成下列步驟。

發布遊戲

建構 APK,並在 Play 管理中心發布遊戲。

  1. 在 Android Studio 選單中,依序選取「Build」>「Build Bundle(s) / APK(s)」>「Build APK(s)」
  2. 發布遊戲。 詳情請參閱「 透過 Play 管理中心發布私人應用程式」。