ゲーム開発の備忘録

趣味のゲーム開発でのノウハウや、技術的に嵌ったポイントを忘れないように書き記しておくブログです。

Google Play Games Servicesサインインの実装 コーディング編(2018/08/28時点)

 環境構築を実施していない方は、先に環境構築編からご確認ください。
deep-verdure.hatenablog.com

Google Play Servicesの利用可否の確認

 いよいよ、Google Play Games Servicesのサインイン処理のコーディングに入りますが、その前に、ユーザのAndroid端末がGoogle Play Servicesに対応しているかどうかを確認しておく必要があります。
以下にコードを掲載します。(本記事のコードは全てKotlinで記述しています。)

   private fun isGoogleApiAvailable() :Boolean{
        val availability = GoogleApiAvailability.getInstance()
        val result = availability.isGooglePlayServicesAvailable(activity)
        return when(result){
            ConnectionResult.SUCCESS ->
                true
            ConnectionResult.API_VERSION_UPDATE_REQUIRED,
            ConnectionResult.SERVICE_MISSING,
            ConnectionResult.SERVICE_DISABLED -> {
                val dialog = availability.getErrorDialog(
                        activity, result, REQUEST_CODE_AVAILABILITY, null)
                dialog.show()
                false
            }
            else -> {
                showErrorDialog("Please connect to internet!!")
                false
            }
        }
    }

 Google Play Servicesがそのままでは利用できない状態にある時は、Googleが用意した専用のダイアログを呼び出します。ユーザは、そのダイアログの指示に従い、Google Play Servicesアプリのインストールやバージョンアップなどを実施します。11行目で渡しているREQUEST_CODE_AVAILABILITYは、自前で用意したリクエストコードです。
 ダイアログ経由の操作が完了すると、自作アプリに処理が返ってきますので、ActivityのonActivityResult()でキャッチします。

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

        if(requestCode == REQUEST_CODE_AVAILABILITY){
           signIn() //サインインメソッド。先頭で isGoogleApiAvailable()を呼んでいる。
        }
    }

 処理が返ってきたら、Google Play Servicesが利用可能な状態になったかを再度確認し、問題なければサインイン処理に進みます。

サインイン処理の流れ

 サインイン処理のメソッドを以下に示します。

    private val lastSignedInAccount = GoogleSignIn.getLastSignedInAccount(activity)

    fun signIn(){
        if(!isGoogleApiAvailable()){
            return
        }

        if(lastSignedInAccount == null) {
            silentSignIn()
        }
        else{
            afterConnectedProcesses(lastSignedInAccount)
        }
    }

 signIn()の先頭でGoogle Play Servicesの利用可否を調べ、問題なければサインイン処理に進みます。afterConnectedProcesses()は、自前で実装した、サインイン後の処理の窓口の役割を持つメソッドです。
GoogleSignIn.getLastSignedInAccount()を用いると、最後にサインインしたアカウント情報をすぐに引き出すことができます。前回、サインインに失敗している場合はnullが返ってきます。8行目で前回、サインインに成功したアカウントがあるかどうかを調べ、アカウントがある場合はサインイン処理を完了としています。アカウントがない場合は、サイレントサインインから実行する必要があります。

 サインイン処理は以下の二段階で試みます。

サイレントサインイン
 ⇒ 過去にサインインしたことがあるアカウント情報を用いて自動的にサインインを実行する。ユーザの負担が少ないメリットがある。GoogleSignIn.getLastSignedInAccount()で直前にサインインしたアカウントがない状態でも、過去にサインインしたことのあるアカウント情報を拾ってサイレントサインインが成功する場合もあり得る。

インタラクティブサインイン
 ⇒ Googleが作成した専用のダイアログを用いて、ユーザが手動でアカウントを選択し、サインインを実施する。インタラクティブサインインも失敗した場合は、サインイン処理全体として失敗したこととなる。

 まず、サイレントサインインを試し、失敗したらインタラクティブサインインを行う流れです。最初に、どちらのサインイン方式でも必要となる、サインインクライアントの生成から説明します。が、その前に事前にお伝えしておきたいことが1つあります。

GoogleSignInクラスとGoogleApiClientクラス

 ネット上でGoogle Play Servicesのサインイン処理について調べていると、GoogleSignInクラスを用いたコードと、GoogleApiClientクラスを用いたコードの2つがヒットすることに気付くと思います。
 GoogleApiClientクラスは2016年にdeprecatedとなり、それ以降はGoogleSignInクラスを使うことになっていますので、間違えないように注意しましょう!以下の公式マニュアルでも、GoogleApiClientクラスがdeprecatedされていることが分かります。

Accessing Google APIs with GoogleApiClient (deprecated)  |  Google APIs for Android  |  Google Developers

 かなりの割合でGoogleApiClientクラスを用いた記事が見つかるので、なかなか情報を探すのに苦労します……

サインインクライアントの生成

 サインイン処理で共通して必要となるサインインクライアントの生成コードを以下に示します。

    private val signInOptions = GoogleSignInOptions
            .Builder(GoogleSignInOptions.DEFAULT_GAMES_SIGN_IN)
            .requestEmail()
            .requestIdToken(parent.getString(R.string.default_web_client_id))
            .build()
    private val signinClient =
            GoogleSignIn.getClient(activity, signInOptions)

 サインインクライアントを直接生成しているのは、6,7行目です。GoogleSignIn.getClient()でサインインクライアントを取得できます。
 GoogleSignInOptionsクラスのインスタンスを渡すことで、サインイン時の様々なオプションを設定することができます。Google Play Games Servicesにサインインする場合、GoogleSignInOptions.DEFAULT_GAMES_SIGN_INを指定し、requestEmail(), requestIdToken()でユーザのEメールアドレス情報、認証トークンを要求しておく必要があります。

 ここで、R.string.default_web_client_idには、OAuth2.0 Web Client IDが格納されています。RとあるのでAndroidリソースな訳ですが、このリソースの本体は {application}/build/generated/res/google-services/{build_type}/values/values.xml の位置にあります。このvalues.xmlは、Androidプロジェクトのルートディレクトリに配置したgoogle-services.json をもとに、ビルド時に自動生成されます。このために必要だったんですね!

サイレントサインインの実装

 サイレントサインインの実装メソッドを以下に示します。

    private fun silentSignIn(){
        signinClient.silentSignIn().addOnCompleteListener(activity) {
            if (it.isSuccessful) {
                afterConnectedProcesses(it.result)
            } else {
                interactiveSignIn()
            }
        }
    }

 GoogleSignInClientクラスの、silentSignIn()メソッドを呼ぶと裏でサイレントサインインを実施してくれます。サイレントサインインは非同期で実行されます。
 サイレントサインインに限らず、Google Play Services APIにおいて非同期処理される全てのメソッドは、Tasks APIという、Googleが用意した非同期処理用APIを利用しています。Tasks APIの説明については、以下の公式マニュアルを参照してください。

The Tasks API  |  Google APIs for Android  |  Google Developers

 上記のサイレントサインインの例では、非同期処理完了時のイベントリスナであるaddOnCompleteListener()に、サイレントサインイン成功時にサインイン処理を完了して次に進むか、サイレントサインイン失敗を受けてインタラクティブサインインを実施するかを選択するラムダを渡しています。
 因みに、一応補足しておくと、Kotlinでは引数リスト末尾のラムダの定義を、メソッドの引数リスト定義の後続に移し、可読性を高めることができます。また、ラムダの引数がただ一つの場合、暗黙のパラメータitが自動生成され、それを利用することができます。3行目のitが暗黙のパラメータに該当し、今回の例では非同期処理結果等を格納するTaskクラスのパラメータとなっています。

インタラクティブサインインの実装

 インタラクティブサインインの実装メソッドを以下に示します。

    private fun interactiveSignIn(){
        val signinIntent = signinClient.signInIntent
        activity.startActivityForResult(signinIntent, REQUEST_CODE_SIGN_IN)
    }

 インタラクティブサインインに必要なインテントを、GoogleSignInClientクラスの、signInIntentから取得することができます。あとは、そのインテントを設定しつつActivityを新たに起動すればOKです。REQUEST_CODE_SIGN_INは自前で用意したリクエストコードです。

 ユーザがアカウント選択ダイアログでアカウントを選択すると、自作アプリに処理が返ってきますので、ActivityのonActivityResult()でキャッチします。

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

        if(requestCode == REQUEST_CODE_SIGN_IN){
            signInWithIntentResult(data) //ここを追加しました
        }
        else if(requestCode == REQUEST_CODE_AVAILABILITY){
            signIn()
        }
    }

 signInWithIntentResult()の定義を以下に示します。

    fun signInWithIntentResult(data: Intent?){
        val task = GoogleSignIn.getSignedInAccountFromIntent(data)
        if (task.isSuccessful){
            val account = task.result
            afterConnectedProcesses(account)
        } else {
            showErrorDialog(task.exception?.message) //自作のエラーダイアログ表示メソッド
        }
    }

 GoogleSignIn.getSignedInAccountFromIntent()インタラクティブサインインの結果を取得できます。ここでもTasks APIが利用されていますが、このメソッドは少々特殊で、非同期処理の完了を待つ必要がありません。そのため、3行目でいきなりTaskクラスのisSuccessfulにアクセスしています。
 インタラクティブサインインに成功した場合は、サインイン処理成功とみなし、次に進みます。インタラクティブサインインに失敗した時は、サインイン処理失敗とみなし、エラーダイアログを表示して終了します。

サインイン処理成功後の処理

 サインイン処理成功後は、プレイヤーの取得を行います。
 

    private fun afterConnectedProcesses(account :GoogleSignInAccount?){
        if(account == null){
            AlertDialog.Builder(activity)
                    .setMessage("Signed-In failured!!")
                    .setNeutralButton("OK", null)
                    .show()
        }else{
            retrievePlayerInformation(account)
        }
    }

    private fun retrievePlayerInformation(account :GoogleSignInAccount){
        val players = Games.getPlayersClient(activity, account)

        players.currentPlayer.addOnCompleteListener {
            if(it.isSuccessful){
                localPlayer = it.result
                showSignedInPopup(account) //ポップアップ表示メソッド。あとで解説する。
            }
            else{
                showErrorDialog(it.exception?.message)
            }
        }

 プレイヤーとは、Google Play Games Servicesにおけるユーザの概念です。アカウントは全てのGoogle Play Servicesで共有ですが、アカウントにはGoogle Play Games Servicesに関連する情報が含まれていません。そこで、Google Play Games Servicesを利用する際は、アカウントからプレイヤーを取得する必要があります。
 
 1つのアカウントに複数のプレイヤーが含まれる場合があるため、まずはプレイヤー群を取得します。Games.getPlayersClient()メソッドでプレイヤー群を取得できます。プレイヤー群を取得したら、大抵の場合はPlayersClientクラスのcurrentPlayerから、直近で利用していたプレイヤーを取得します。この際も、Tasks APIが利用されているので注意しましょう。

サインイン処理成功時のポップアップの表示

 サインイン処理成功時のポップアップを表示することができます。以下に実装例を示します。

    private fun showSignedInPopup(account :GoogleSignInAccount){
        val gamesClient = Games.getGamesClient(activity, account)
        
        //TODO ここに、ポップアップ表示用のViewを取得するコードを記述してください。

        gamesClient.setGravityForPopups(Gravity.TOP or Gravity.CENTER_HORIZONTAL)
        gamesClient.setViewForPopups(view) //上で取得したViewと紐づける
    }

★おまけ:libGDXの場合
 libGDXでは、GoogleSignInを利用してサインイン処理を実装した場合、上記の実装を用いてもポップアップを表示することができません!そのため、現状ではdeprecatedされたGoogleApiClientを用いてサインイン処理を実装し、上記の実装でポップアップを表示するか、Toast等で自前実装するしかありません。私はdeprecatedされたAPIを使いたくなかったので、泣く泣く以下のようなToastでの実装をしました。

    private fun showSignedInPopup(){
        activity.runOnUiThread {
            val toast = Toast.makeText(activity,
                    "Googleアカウント:「" + localPlayer.displayName + "」でサインインしました。",
                    Toast.LENGTH_LONG)
            toast.setGravity(Gravity.TOP or Gravity.CENTER_HORIZONTAL, 0, 0)
            toast.show()
        }
    }

自前実装の場合、GamesClient利用時と異なり、アカウントからプレイヤーを先に取得しておかないと、displayName等の情報を取得することができません。Google Play Games Servicesサインインの場合、サインイン成功後にプレイヤーではなくアカウントのほうのdisplayNameを確認しても、中身がnullになっているので注意が必要です。

サインアウト処理

 サインアウト処理の実装は非常に簡単です。以下のようにGoogleSignInClientクラスのsignOut()メソッドを呼ぶだけでOKです。

    fun signOut(){
        signinClient.signOut()
    }

準備完了

 お疲れ様です!ここまでの実装が完了すれば、Google Play Games Servicesへのサインインが実行できるようになっているはずです。是非、Google Play Games Servicesの様々な機能を使い倒し、開発中のゲームをリッチにしていきましょう!