Unityでスコアサーバーとの送受信をやってみた話

Unityの画面でスコアを入力してサーバーへ登録、サーバーからスコアの一覧を取得してUnityの画面へ出力するということをやってみました。スコアサーバーはRuby on Railsで立てて、UnityではJson.NETとUniRxを用いました。特にやりたいことがあったわけではないのですが、知見のためにやりました。

やりたいこと

  1. Unityの画面から名前とスコアを入力してサーバーへ送信する。
  2. サーバーから名前とスコアの一覧を取得してUnityの画面へ表示する。

Ruby on Railsでスコアサーバーを立てる

名前とスコアが登録できればいいので、雑ですがscaffoldで以下のようにしました。

rails g scaffold User name:string score:integer

また、バリデーションを書きました。

user.rb

class User < ApplicationRecord
    validates :name, 
        presence: true, 
        length: {minimum:3, maximum:15}, 
        format: { with: /\A[a-zA-Z0-9_]+\z/ }
    validates :score,
        presence: true,
        numericality: {only_integer: true, greater_than_or_equal_to: 0, less_than: 10000000}
end

この状態でサーバーを立ち上げればJSONでデータがやりとりできます。curlでデータを取得、送信するサンプルを置いておきます。

スコアデータの一覧を取得

curl http://URL/users

スコアデータの送信

curl -X POST -H "Content-Type: application/json" -d '{"user": {"name": "shira", "score": 456 }}' http://URL/users

Unityで通信用のクラスを作る

JSONをシリアライズ・デシリアライズするときの受け皿となるクラスを作成します。この辺Json.NETの書き方に従います。

UserData.cs

public class UserData
{
    [JsonProperty("name")]
    public string Name { get; set; }
    [JsonProperty("score")]
    public int Score { get; set; }
}

サーバーと送受信するクラスを作成します。UniRxを用いてObservableを返すような静的メソッドを用意しました。

Scorer.cs

public class Scorer
{
    private static readonly string URL = "http://URL";

    private static readonly Dictionary<string, string> Headers = new Dictionary<string, string>
    {
        { "Content-Type", "application/json"}
    };

    public static IObservable<string> ObservablePostUser(UserData userData)
    {
        var o = new JObject();
        o.Add("user", JObject.FromObject(userData));
        return ObservableWWW.Post(URL + "/users", Encoding.UTF8.GetBytes(o.ToString()), Headers);
    }

    public static IObservable<List<UserData>> ObservableGetUsers()
    {
        return ObservableWWW.Get(URL + "/users")
                            .Select(_ => JsonConvert.DeserializeObject<List<UserData>>(_));
    }
}

Unityでスコア登録画面を作る

uGUIで記事上部のように要素を配置しました。

入力側でもバリデーションチェックをしていきます。名前とスコアが正しく入力できていないときはボタンをクリックできないようにします。以下のスクリプトを「Create Empty」で作成した空のGameObjectに貼り付けて、インスペクタービューから対応するuGUIのオブジェクトを選択します。ここではTextの代わりにTextMeshProを使っていますが、置き換えて考えて大丈夫です。また、MonoBehaviourの代わりにSingletonMonoBehaviourを用いていますが、これはヒエラルキーに同じオブジェクトが2つ以上できないように抑制するもので、これもMonoBehaviourに置き換えて考えて大丈夫です。

RegisterUIManager.cs

public class RegisterUIManager : SingletonMonoBehaviour<RegisterUIManager>
{

    [SerializeField]
    private Button registButton;

    [SerializeField]
    private TextMeshProUGUI nameInputFieldText;

    [SerializeField]
    private TextMeshProUGUI scoreInputFieldText;

    [SerializeField]
    private TextMeshProUGUI messageText;

    // Use this for initialization
    void Start()
    {
        messageText.text = "";

        // nameとscoreのバリデーション
        Observable.Merge(nameInputFieldText.ObserveEveryValueChanged(_ => _.text).AsUnitObservable())
                  .Merge(scoreInputFieldText.ObserveEveryValueChanged(_ => _.text).AsUnitObservable())
                  .Subscribe(_ =>
                  {
                      int n;
                      // どちらも空白でない場合のみボタンをクリックできるようにする
                      registButton.interactable = nameInputFieldText.text.Trim() != ""
                                    && scoreInputFieldText.text.Trim() != ""
                                    && int.TryParse(scoreInputFieldText.text.Trim(), out n);

                  })
                  .AddTo(gameObject);

        // Submitボタンの挙動
        registButton.onClick.AsObservable()
                    .Subscribe(_ =>
                    {
                        // サーバーとの送受信中はボタンをクリックできないようにする
                        registButton.interactable = false;
                        messageText.text = "Submitting...";

                        // バリデーションチェックが済んでいる前提
                        string name = nameInputFieldText.text.Trim();
                        int score = int.Parse(scoreInputFieldText.text);

                        // サーバーにデータを送信する
                        Scorer.ObservablePostUser(new UserData { Name = name, Score = score }).Subscribe(x =>
                        {
                            messageText.text = "Submitted!";
                            Debug.Log(x);
                        }, ex =>
                        {
                            messageText.text = "Error!";
                            Debug.LogError(ex);
                        }, () =>
                        {
                            // OnCompletedの一定秒数後に再度ボタンをクリックできるようにする
                            Observable.Timer(TimeSpan.FromSeconds(2)).Subscribe(x =>
                            {
                                messageText.text = "";
                                registButton.interactable = true;
                            });
                        });

                    });


    }

    // Update is called once per frame
    void Update()
    {

    }
}

Unityでスコア一覧画面を作る

以下のような表示画面を作りました。

ScrollViewなどを用いていますが、その辺りの説明は省くとして、コード部分の説明のみ書きます。RegisterUIManager.csと同様に空のオブジェクトを作成して以下コードを貼り付けます。scrollViewContentにScrollViewのcontentオブジェクトを指定し、nodeにプレハブ化したScrollViewの入れ子要素を指定しています。

ViewerUIManager.cs

public class ViewerUIManager : SingletonMonoBehaviour<ViewerUIManager>
{
    [SerializeField]
    private GameObject scrollViewContent;

    [SerializeField]
    private GameObject node;

    // Use this for initialization
    void Start()
    {
        Scorer.ObservableGetUsers().Subscribe(list =>
        {
            list.ForEach(userData =>
            {
                var obj = Instantiate(node, Vector3.zero, Quaternion.identity, scrollViewContent.transform);
                obj.GetComponent<NodeBehaviour>().SetData(userData);
            });
        });

    }

    // Update is called once per frame
    void Update()
    {

    }
}