Unity + NCMBでスコアランキングを実装する

#はじめに
この前Unityネットワーク完全に理解したに行ってきてMBaaSに関する知見を得てきました。あの会メンツがやべーよ。
<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">ここ数週間制作していたバベルの塔ぶっ壊しVRアクション、「BABEL UP!!」のβ版をboothにて無料公開しました！　オンラインランキングがついているのでぜひ遊んで競ってみてください！ご意見・感想などくれると嬉しいです<a href="https://t.co/gUW2HgvaLa">https://t.co/gUW2HgvaLa</a> <a href="https://t.co/2ZGLQTqStN">pic.twitter.com/2ZGLQTqStN</a></p>&mdash; 避雷 (@lucknknock) <a href="https://twitter.com/lucknknock/status/1075421698709643265?ref_src=twsrc%5Etfw">2018年12月19日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
このゲームでNCMBでユーザランキングを実装したので得た知見を公開します。
#NCMBとは
[ニフクラ](https://cloud.nifty.com/)が提供しているモバイルバックエンドです
https://mbaas.nifcloud.com/
。パッケージの容量が小さい、適当に書いても動作する、JSONの知識も不要等簡単に動く点でモバイルバックエンド系の入門として最高だそうです。モバイルと名乗っているものの、実際はPC向けビルドでもエディタ上でもガンガン動作するのでお強い。詳しい説明はこの記事が良いです。
[Unity対応してるmBaaS 全部紹介する](https://speakerdeck.com/takaaki_ichijo/unitydui-ying-siterumbaas-quan-bu-shao-jie-suru-haiparinkugaji-neng-sitenaiban)
公式ドキュメントはこちら。こっちの方が詳しいです。
https://mbaas.nifcloud.com/doc/current/index.html
#実装
##NCMB側の設定
###NCMBに登録する
NCMBの公式ページに飛んでユーザ登録をしましょう。GoogleアカウントやTwitterアカウントで登録ができます。便利。
<img width="530" alt="スクリーンショット 2018-12-28 16.01.31.png" src="https://qiita-image-store.s3.amazonaws.com/0/256231/1c8eeae7-9dde-bde0-f87d-840dd027ef07.png">

###NCMBのデータベースにクラスを追加する
NCMBダッシュボードを開いて、データストアタブからクラスの横の+作成をクリックして新しいクラスを登録します
<img width="167" alt="スクリーンショット 2018-12-28 16.02.49.png" src="https://qiita-image-store.s3.amazonaws.com/0/256231/02ea3e52-48e6-c62b-431f-63fa54d33cbf.png">
<img width="535" alt="スクリーンショット 2018-12-28 16.04.27.png" src="https://qiita-image-store.s3.amazonaws.com/0/256231/108ea36c-ed5b-0357-7f3d-ab12cba653e6.png">

###NCMBのクラスに要素を追加する
新しいフィールドを追加すると新しい要素をクラスに追加することができます。追加された要素の型についてはデータの登録時に推測してくれるみたいです。
<img width="490" alt="スクリーンショット 2018-12-28 16.04.14.png" src="https://qiita-image-store.s3.amazonaws.com/0/256231/f99f9a55-5156-5923-8c1c-2bd96c4d25be.png">
<img width="504" alt="スクリーンショット 2018-12-28 16.04.37.png" src="https://qiita-image-store.s3.amazonaws.com/0/256231/3980bcb0-676d-d2f3-e374-002929bef55a.png">

##Unity側の設定
###パッケージのインポート
https://github.com/NIFCloud-mbaas/ncmb_unity/releases
ここからNCMB向けのUnityパッケージをダウンロードしてプロジェクトに取り込んでください。
###NCMBのマネージャを設置
空のゲームオブジェクトの名前を"NCMBSettings"にしてNCMBSettingsコンポネントをアタッチします。
ここで注意すべきは、NCMBは内部処理的に"NCMBSettings"の名前でゲームオブジェクトを検索する仕様になっているので名前が違うと動かないみたいです。注意です(敗北者は語る)。
![キャプチャ118.PNG](https://qiita-image-store.s3.amazonaws.com/0/256231/0734d4dd-52ce-891c-712d-2ac5086cefe6.png)

###NCMB連携情報の入力
<img width="673" alt="スクリーンショット 2018-12-28 16.06.50.png" src="https://qiita-image-store.s3.amazonaws.com/0/256231/d055d6c6-d8b7-20b1-89a2-0617b85cd97e.png">
アプリ一覧を開けばアプリケーションキーとクライアントキーが見られるのでコピペでNCMBSettingコンポネントのインスペクタに貼り付けましょう
###using NCMBを追加
可読性と利便性のため今後NCMBを扱うコードには

```
using NCMB;
```

を追加しましょう
##データの登録
### 新しいNCMBオブジェクトを生成
NCMBではサーバーにNCMBオブジェクトを送信してサーバー上のDBを弄っていきます。

```C#
NCMBObject obj = new NCMBObject("HighScore");
```

で新しいNCMBオブジェクトを生成します。　コンストラクタの引数はサーバー側に登録したクラス名にしてください。
### 生成したNCMBオブジェクトにパラメータを代入する
先ほど生成したNCMBオブジェクトにユーザ名とスコア情報を代入します。

```C#
obj["Name"]  = name;
obj["Score"] = 0;
```

### 代入したNCMBオブジェクトをサーバーに保存する
データは入ったのでこれをサーバーに送信してみましょう。

```C#
obj.SaveAsync();
```

関数名通り送信は非同期で行われています。

```C#
obj.SaveAsync((NCMBException e) => {      
    if (e != null) {
        //エラー処理
    } else {
        //成功時の処理
        
    }                   
});
```
とするとラムダ式でセーブ後の処理をできます。

##データの取得
サーバーから保存したデータを取得してみましょう。
###取得するオブジェクトを選択
取得したデータはNCMBQueryに格納されるのでこれを先に生成しておきます。

```C#
NCMBQuery<NCMBObject> query = new NCMBQuery<NCMBObject> ("HighScore");
```

コンストラクタの引数はサーバ側に登録したクラス名と同じにしてください。
同様にラムダ式の記述も可能です。
###取得時設定をする（ソート、絞り込み等）
新しいNCMBQueryの生成から実際に取得するまでの間に条件を指定することで条件にあった形のクエリデータを取得できます。
このように絞り込みの条件を重ねて表現できます。

```C#
// Score = 7 
query.WhereEqualTo("Score",7);

// Score != 7
query.WhereNotEqualTo ("Score", 7);

// Score < 5
query.WhereLessThan("Score",5);

// Score <= 5
query.WhereLessThanOrEqualTo("Score",5);

// Score > 5
query.WhereGreaterThan("Score",5);

// Score >= 5
query.WhereGreaterThanOrEqualTo("Score",5);
```

配列を使うことでandやorを表現することもできます

```C#
NCMBQuery<NCMBObject> query = new NCMBQuery<NCMBObject> ("QueryTest");
List<int> request =  new List<int> { 1, 5, 10};

//1,5,10のいずれかと一致するオブジェクトを検索
query.WhereContainedIn("Score",request);

//1,5,10のいずれとも一致しないオブジェクトを検索
query.WhereNotContainedIn("Score",request);

//配列に1,5,10のいずれかが含まれているオブジェクトを検索
query.WhereContainedInArray("ScoreArray",request);

//配列に1,5,10をすべて含むオブジェクトを検索
query.WhereContainsAll("ScoreArray",request);
```

取得数に制限をかけることができます。　指定した条件の中で並び替え後上位に並ぶものを指定個だけ取得できます。

```C#
//取得件数の指定
query.Limit = 5;

//取得開始位置の指定
query.Skip = 6;
```

並び替え順を任意に指定することができます。複数条件での並び替えも可能

```C#
// 昇順
query.OrderByAscending ("Score");

// Scoreでの昇順でソートしたあと、Ageでの昇順でソートする
NCMBQuery<NCMBObject> query = new NCMBQuery<NCMBObject> ("SortTest");
query.OrderByAscending ("Score");   //Scoreを昇順で並べ替え
query.AddAscendingOrder ("Age");    //さらにAgeを昇順で並べ替え

// 降順
query.OrderByDescending ("Score");

// Scoreでの降順でソートしたあと、Ageでの降順でソートする
NCMBQuery<NCMBObject> query = new NCMBQuery<NCMBObject> ("SortTest");
query.OrderByDescending ("Score");  //Scoreを降順で並べ替え
query.AddDescendingOrder ("Age");   //さらにAgeを降順で並べ替え
```


###実際に取得する

```C#
query.FindAsync ((List<NCMBObject> objList ,NCMBException e) => {

        //検索成功したら
        if (e == null) {
          objList[0]["Score"] = score;
          objList[0].SaveAsync();
        }
      });
```
サーバーサイドからクエリに目的のクラスのDBをダウンロードできました。
処理は非同期で行われるのでデータがサーバーから到着する前に処理を進めると意図しない結果になる点に注意しましょう。
##データの書換
NCMBではObjectIDというユニークな文字列が各データに与えられます。これを手がかりに自分のデータを取得、書き換えて再提出してみます。
###一番最初にデータを登録する
この時に割り当てられたObjectIDを取得しておきます。
obj.SaveAsync()が成功した時、objには保存先のObjectIDが保存されるので

```C#
NCMBObject obj = new NCMBObject("Score");
obj["score"] = score; //ゲームのスコア
string selfID;
obj.SaveAsync((NCMBException e) => {      
    if (e != null) {
        //エラー処理
    } else {
        //成功時の処理
        selfID = obj.ObjectId;
    }                   
});
```

で保存先のObjectIdを取得することができます。これを参照することで書き換えるデータを特定します。
###データを取得する
先ほど述べたとおりにすれば取得できます。
###データを書き換える
これをクライアント側で書き換えます。保存の時と同じ要領で書き換えて行けば良いです。

```C#
NCMBObject obj = new NCMBObject ("Score");
obj.ObjectId = selfID;
obj.FetchAsync ((NCMBException e) => {        
    if (e != null) {
        //エラー処理
    } else {    
        //成功時の処理
        obj["Score"] = newScore;//新しいスコア
    }               
});
```
また、次のように取得することもできます。

```C#
query.WhereEqualTo("objectId",userID);
        Debug.Log(userID);
        query.FindAsync((List<NCMBObject> objList, NCMBException e) => {
            if(e == null)
            {
                if(objList.Count == 0)
                {
                    //存在しない
                    Debug.Log("Obj is not found");
                }
                else
                {
                    objList[0]["name"] = userName;
                    objList[0]["userHighScore"] = userHighScore;
                    objList[0].SaveAsync();
                }

            }
            else
            {
                
            }

        });
```

FetchAsyncは指定されたオブジェクトの取得、
FindAsyncは絞り込みをかけたクエリを検索して取得、
という違いがあるみたいです。
###データを保存する
「データの保存」のパートでやった通りにデータを保存します。

```C#
obj.SaveAsync();
```

書き換えたQueryをそのままSaveAsyncすれば自動的に書き換えられます。便利〜
##ランキング表示
スコアの要素を降順に並べてクエリを取得すればランキング形式になったスコアデータが得られます。
そのまま上から5つを取得すればナンバー５が得られるしObjectIdで検索して自分の位置を特定すれば自分の前後２位分を表示することもできます。
先ほどのlimitなどを使えばいい感じに切り出されたクエリを取得できそうですが、NCMBには無料で使えるコール回数に限りがあるので全体で取得してクライアント側で切り出した方が良さそうです。

```C#
        query.FindAsync((List<NCMBObject> objList, NCMBException e) => {
            if (e == null)
            {
                int myPosition = 0;
                for(int i = 0; i < objList.Count; i++)
                {
                    if (objList[i].ObjectId == UserManager.Instance.userID)
                    {
                        myPosition = i;
                        break;
                    }
                }
                for(int i = 0; i < Math.Min(objList.Count, 5); i++)
                {
                    topRanking += i + ":" + objList[i]["name"] +"   "+ objList[i]["userHighScore"] + "\n";
                }
                for(int i = Math.Max(0,myPosition - 2); i < Math.Min(objList.Count,myPosition + 3); i++)
                {
                    myRanking += i + ":" + objList[i]["name"] + "   " + objList[i]["userHighScore"] + "\n";
                }

            }
            else
            {
                topRanking = "ERROR!ネットワーク接続に問題が発生しました。";
                myRanking = "ERROR!ネットワーク接続に問題が発生しました。";
            }

        });
```

#最後に
めっちゃイージーにネットワークシステムを構築することができました。非リアルタイム型のオンライン要素を追加する時に触ってみると幸せになれそう。ちょっと前に流行ったフリマシステムとかもこれで作れそう。セキュリティ面については少し考える必要がありそうです。オチが思いつかないのでこれでおわり。