【Unity】【PlayFab】環境入れたその次に。実際の利用でのログインとユーザーデータ更新


まえがき

PlayFabの入門情報などが増えてきて簡単に使えることまでは分かったが、
では実際に組み込むには具体的にどう書いたら良いの?
というところの情報が意外と少なかったので実際に使う事を想定してコード書いてみました。

PlayFab環境の導入までは済んでいる前提です。
公式

UnityでPlayFabを使い始める方法(インストール〜匿名ログイン)
この辺りがわかりやすいかと思います。

下記コンポーネントを適当なGameObjectにアタッチして使う想定です。

まずはコード全貌

PlayFabManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using PlayFab;
using PlayFab.ClientModels;

public class PlayFabManager : MonoBehaviour {

    /// <summary>
    /// PlayFabに保存するユーザーデータのキー
    /// </summary>
    private string[] PLAYFAB_KEY_STR = new string[] {
        "PlayFabId",
        "Name",
        "RankingValue"
    };
    /// <summary>
    /// PLAYFAB_KEY_STRにアクセスするためのenum
    /// </summary>
    private enum KEY_STR
    {
        ID,
        NAME,
        RANKING_VALUE,
    }
    /// <summary>
    /// クライアント側でユーザーデータ(取得データ)を管理しやすくするためのclass
    /// </summary>
    public class PlayFabUserData {
        public bool getted = false;

        public string id;
        public string name;
        public int rankingValue;
    }

    private int m_retryCnt = 0;
    private int MAX_CNT_RETRY = 10;


    static public PlayFabUserData s_myUserData { private set; get; } = null;

    static public void SetUserData(string _id = null, string _name = null, int _rankingValue = -1)
    {
        if (null == s_myUserData)
        {
            s_myUserData = new PlayFabUserData();
        }

        if (null != _id)
        {
            s_myUserData.id = _id;
        }
        if (null != _name)
        {
            s_myUserData.name = _name;
        }
        if (-1 != _rankingValue)
        {
            s_myUserData.rankingValue = _rankingValue;
        }
    }

    void Start()
    {
        StartCoroutine(LoginCol());
    }


    public IEnumerator LoginCol(bool _forceCreateID = false)
    {
        // タッチできないようにガード用フェードなど入れる
        GameCommonManager.GetInstance().m_loadingMarkManager.ActiveBlueLoadingMark();

        // セーブになくて新しく作ったIDか
        bool isNewID = false;       
        string loadPlayFabID = SaveLoad.Load<string>(SaveLoad.SAVE_KEY.PLAYER_ID_FOR_PLAYFAB);

        if (null == s_myUserData)
        {
            s_myUserData = new PlayFabUserData();
        }

        // コールバック受け取り待ちフラグ
        s_myUserData.getted = false;

        // 既にIDを持っている
        if (!_forceCreateID && false == string.IsNullOrEmpty(loadPlayFabID))
        {
            s_myUserData.id = loadPlayFabID;
        }
        // 今回がはじめて
        else
        {
            s_myUserData.id = GameUtility.GenerateCustomID();
            isNewID = true;
        }

        Debug.Log("ログイン挑戦 id = " + s_myUserData.id);

        PlayFabClientAPI.LoginWithCustomID(new LoginWithCustomIDRequest
        {
            CustomId = s_myUserData.id,
            TitleId = PlayFabSettings.TitleId,
            CreateAccount = true
        }
        , result =>
        {
            Debug.Log("ログイン成功 id=" + s_myUserData.id);
            s_myUserData.getted = true;

            // セーブになかったのに、サーバーからのレスポンスでは新規アカウントではないと言われる
            // = すでに使われているIDと判断
            if (isNewID && false == result.NewlyCreated)
            {
                Debug.Log("成功したが既に使われているIDだったのでもう一度");
                if (MAX_CNT_RETRY < m_retryCnt++)
                {
                    Debug.Log("ログインリトライMAXオーバーで諦め");
                    return;
                }

                // 既に使われているIDだったのでもう一度
                StartCoroutine(LoginCol(true));
                return;
            }

            if (isNewID)
            {
                // ログイン確定したのでセーブに保存
                SaveLoad.Save<string>(SaveLoad.SAVE_KEY.PLAYER_ID_FOR_PLAYFAB, s_myUserData.id);
            }

        }
        , error =>
        {
            Debug.Log(error.GenerateErrorReport());
            s_myUserData.getted = true;
        });

        // コールバック受け取るまで止めておく
        while (false == s_myUserData.getted)
        {
            yield return null;
        }

        // タッチ許可
        GameCommonManager.GetInstance().m_loadingMarkManager.DeactiveBlueLoadingMark();

        yield break;
    }


    /// <summary>
    /// ユーザーデータ更新
    /// </summary>
    [ContextMenu("UpdateUserData")]
    void UpdateUserData()
    {
        var requestPrivate = new UpdateUserDataRequest
        {
            Data = new Dictionary<string, string>()
            {
                {PLAYFAB_KEY_STR[(int)KEY_STR.ID], s_myUserData.id},
                {PLAYFAB_KEY_STR[(int)KEY_STR.NAME], "hoge" },//s_myUserData.name},
            },
            //アクセス許可設定 Key単位で変えたい場合は別のリクエスト作らないとだめぽい
            Permission = UserDataPermission.Private
        };

        PlayFabClientAPI.UpdateUserData(
            requestPrivate
            , result =>
            {
                Debug.Log("Private属性のプレイヤーデータの更新成功");
            }
            , error =>
            {
                Debug.Log(error.GenerateErrorReport());
            }
        );
    }


    /// <summary>
    /// 自分のユーザーデータ取得(id以外)
    /// </summary>
    public void GetMyUserData()
    {
        s_myUserData.getted = false;

        PlayFabClientAPI.GetUserData(
            new GetUserDataRequest()
            {
                PlayFabId = s_myUserData.id
            }
            , result =>
            {
                s_myUserData.name = result.Data[PLAYFAB_KEY_STR[(int)KEY_STR.NAME]].Value;
                s_myUserData.rankingValue = int.Parse(result.Data[PLAYFAB_KEY_STR[(int)KEY_STR.RANKING_VALUE]].Value);

                s_myUserData.getted = true;
            }
            , error =>
            {
                Debug.Log(error.GenerateErrorReport());
                s_myUserData.getted = true;
            }
        );
    }


    /// <summary>
    /// タイトルデータ(マスターデータ)の取得
    /// </summary>
    [ContextMenu("GetTitleData")]
    void GetTitleData()
    {
        PlayFabClientAPI.GetTitleData(new GetTitleDataRequest()
            , result =>
            {
                string key = "TitleDataAAA";
                if (result.Data.ContainsKey(key))
                {
                    Debug.Log(key + ": " + result.Data["TitleDataAAA"]);
                }
                else
                {
                    Debug.Log("マスターデータ無かった key=" + key);
                }
            }
            , error =>
            {
                Debug.Log(error.GenerateErrorReport());
            }
         );
    }


    [ContextMenu("Test_GetMyUserData")]
    private void Test_GetMyUserData()
    {
        StartCoroutine(Test_GetMyUserDataCol());
    }
    private IEnumerator Test_GetMyUserDataCol()
    {
        GetMyUserData();

        while (false == s_myUserData.getted)
        {
            yield return null;
        }

        Debug.Log("GetMyUserData done: \n name =" + s_myUserData.name);
        Debug.Log("id =" + s_myUserData.id);

        yield break;
    }
}


ログイン

まずはログインするためにユニークなIDをクライアント側で作る必要があります。
実はここが一番勘違いしていたところで、てっきり通信レスポンスでPlayerIDみたいなのを受け取ってそれを使うのかと勘違いしていました。

        PlayFabClientAPI.LoginWithCustomID(new LoginWithCustomIDRequest
        {
            CustomId = m_myUserData.id,
            TitleId = PlayFabSettings.TitleId,
            CreateAccount = true
        }

ここのCustomIdに入れた文字列がIDとなるのですが、初学書だとなぜか固定になっていたり、違う値を入れたら違う人としてログインできるという情報止まりになっているパターンが多かったです。
どうすれば良いかというと、ここで各個人でユニークなIDを指定してやれば良い訳ですが、スマホの端末IDなどは変わる可能性があるという事で、kanさんのブログを拝見してその場で生成する方法を取りました。

GameUtility.cs
    /// <summary>
    /// ユニークIDの生成
    /// </summary>

    //IDに使用する文字
    private static readonly string ID_CHARACTERS = "0123456789abcdefghijklmnopqrstuvwxyz";

    //IDを生成する
    static public string GenerateCustomID()
    {
        int idLength = 32;
        StringBuilder stringBuilder = new StringBuilder(idLength);
        var random = new System.Random();

        //ランダムにIDを生成
        for (int i = 0; i < idLength; i++)
        {
            stringBuilder.Append(ID_CHARACTERS[random.Next(ID_CHARACTERS.Length)]);
        }

        return stringBuilder.ToString();
    }

ほぼkanさんのブログにあった通りです。
予め使う文字列を決めておいて、その中から1文字ずつランダムで選択していき、32文字のランダムなIDを生成しています。
ユニークなIDを生成する方法としては、このような方法の他にも、現在時刻を取得してそれを利用する方法などがあるそうですが、推測されやすくなりそうなので今回はやめました。
余談ですが、こういうところでstringBuilderを使わずに+合成してしまうと激重処理になるのでやめましょう

リトライ

そしてこの生成したIDを元にログインをしますが、万が一すでに使われているIDだったら、IDを再度作り直してリトライするという流れにしています。

public IEnumerator LoginCol(bool _forceCreateID = false)
    {
        // ~中略~

        , result =>
        {
            Debug.Log("ログイン成功 id=" + m_myUserData.id);

            // セーブになかったのに、サーバーからのレスポンスでは新規アカウントではないと言われる
            // = すでに使われているIDと判断
            if (isNewID && false == result.NewlyCreated)
            {
                Debug.Log("成功したが既に使われているIDだったのでもう一度");
                if (MAX_CNT_RETRY < m_retryCnt++)
                {
                    Debug.Log("ログインリトライMAXオーバーで諦め");
                    return;
                }

                // 既に使われているIDだったのでもう一度
                StartCoroutine(LoginCol(true));
                return;
            }

まず前提として、IDが正しく作られたらそのIDはローカルにセーブしておく想定です。
(今回端末へのセーブは自前のSaveLoadクラスを使っています各自自分のに置き換えてください。ただのstring保存です)
つまり、ローカルのセーブ情報としてID情報がなければ新規ログイン(ID生成が必要)ということになります。
それに加えてPlayFabのLoginWithCustomIDRequestのresultに、NewlyCreatedというフィールドがあり、これがtrueだと、今回の処理で新しくIDが作られたということになります。
この二つの情報を組み合わせて、今回用意したIDが他の人が使用中のものでないかどうか判断しています。

[使用中IDパターン]

ローカルのセーブ情報無し = 新規で作るぞ!
PlayFabのResult = 新規じゃなかったよ?

他の人が使用中…

[うまくいったパターン]

ローカルのセーブ情報無し = 新規で作るぞ!
PlayFabのResult = 新規だったよ!

私のアカウントとして使える!

という感じです。この辺りは手探りでやったので認識違いあるかもしれません。

ユーザーデータ更新

続いてユーザーデータ(プレイヤーデータ)の更新です。

こちらはほぼ悩むところはなく、更新するキーとバリューをつめて、APIコールするだけです。

/// <summary>
    /// ユーザーデータ更新
    /// </summary>
    [ContextMenu("UpdateUserData")]
    void UpdateUserData()
    {
        var requestPrivate = new UpdateUserDataRequest
        {
            Data = new Dictionary<string, string>()
            {
                {PLAYFAB_KEY_STR[(int)KEY_STR.ID], m_myUserData.id},
                {PLAYFAB_KEY_STR[(int)KEY_STR.NAME], m_myUserData.name},
            },
            //アクセス許可設定 Key単位で変えたい場合は別のリクエスト作らないとだめぽい
            Permission = UserDataPermission.Private
        };

        PlayFabClientAPI.UpdateUserData(
            requestPrivate
            , result =>
            {
                Debug.Log("Private属性のプレイヤーデータの更新成功");
            }
            , error =>
            {
                Debug.Log(error.GenerateErrorReport());
            }
        );
    }

リクエストを作る時に、Permission = UserDataPermission.Privateの所でその値を他の人が参照できるようにするかどうか決められます。
心情としてはDataのDictionary作っているあたりで一つ一つに対してその場で属性決められたら良いと思いましたが、できなさそう?
requestを分けて2つ書いて送れば問題なくは行けましたがちょっと面倒と感じました。

ログインに関してはこのコンポーネントを付けたGameObjectのStart()時に行っていますが、このユーザーデータ更新に関しては、使用イメージとしては

PlayFabManager.SetUserData(null, "hoge", 500);

としてstaticなs_myUserDataに値をセットしたのち

UpdateUserData();

で反映させています。

またまた余談ですが、[ContextMenu("Test_GetMyUserData")]という様にContextMenu属性をメソッドに付けておくと、UnityEditor実行中に、そのコンポーネントが付いているオブジェクトのインスペクタの…からメソッドをテスト実行できるので、確認に便利です。

ユーザーデータ取得

取得メソッドについては特にそのままなのですが、取得完了したかのフラグをPlayFabUserDataのメンバに持たせています。

    [ContextMenu("Test_GetMyUserData")]
    private void Test_GetMyUserData()
    {
        StartCoroutine(Test_GetMyUserDataCol());
    }
    private IEnumerator Test_GetMyUserDataCol()
    {
        GetMyUserData();

        while (false == s_myUserData.getted)
        {
            yield return null;
        }

        Debug.Log("GetMyUserData done: \n name =" + s_myUserData.name);
        Debug.Log("id =" + s_myUserData.id);

        yield break;
    }

こうすることで、例えば取得処理前にタッチガードを入れて、取得完了したらガードを解くという処理をシーケンシャルにかけるようになります。
PlayFabのコールバックに埋め込んでも良いのですが、そのタイミングでしたい処理が増えてくるとコードが汚くなりがちなので、私は好んでこの形で書くことが多いです。

ログインの方も同じような理由でコルーチンにして、タッチガード入れています。

    private IEnumerator LoginCol(bool _forceCreateID = false)
    {
        // タッチできないようにガード用フェードなど入れる
        GameCommonManager.GetInstance().m_loadingMarkManager.ActiveBlueLoadingMark();

        //〜中略~

        // タッチ許可
        GameCommonManager.GetInstance().m_loadingMarkManager.DeactiveBlueLoadingMark();

        yield break;
    }

話が逸れてしまいますが、ActiveBlueLoadingMark()は、activeにするとクルクルアイコンが回る大きなImageがアクティブになるだけです。(タッチを下に通さない)
1枚作っておくとタッチ制御したい時に呼び出すだけでできるので便利です。

まとめ

■匿名ログインではログインに必要なユニークなIDをクライアント側で作る必要がある
■ユーザーデータの更新はキーとバリューセットしてAPIコールするだけ
■PlayFabのコールバックとコルーチンを組み合わせて処理をシーケンシャルに書きやすくもできる

参考

kanのメモ帳
PlayFabマスターへの道