Cocos2d-xとFirebaseでゲームランキングを作る #1

Cocos2d-xとFirebaseを使って、ゲームランキングを実装してみます。
今回はFirebaseを設定してユーザー登録まで行います。

#プロジェクト作成
Cocos2d-xの導入部分は割愛します。
`cocosコマンド`を使って、新規プロジェクトを作成します。

```
$ cocos new RankingTest -p jp.co.ienter.RankingTest -l cpp -d RankingTest
```
#Firebase導入
[Firebase](https://console.firebase.google.com/?hl=ja)のコンソール画面から、プロジェクトの追加を行います。
プロジェクト名を入力して、`プロジェクトを作成`をクリック。
![s100.png](https://qiita-image-store.s3.amazonaws.com/0/154355/52b89e70-1bc6-5370-3820-3dfb49c571db.png)


今回はiOSアプリでランキングを作成しますので、iOSを選択しましょう。
![s101.png](https://qiita-image-store.s3.amazonaws.com/0/154355/6eb1fc8e-0a2e-d809-8e14-195d5483883e.png)

iOSのバンドルIDを入力しましょう。
Cocos2d-xのプロジェクト作成時に設定したパッケージ名を入力します。
入力後右下の`REGISTER APP`ボタンをクリックしてください。
![s102.png](https://qiita-image-store.s3.amazonaws.com/0/154355/2d9808b6-ee0c-204b-c0eb-203d9f61ea7c.png)

XCodeにFirebase導入に必要なplistファイルをダウンロードします。
ダウンロードしたplistはXCodeプロジェクトに追加しておきましょう。
![s103.png](https://qiita-image-store.s3.amazonaws.com/0/154355/bf5eaa1e-3301-3e6e-8083-514cd8ca91b9.png)

![s103_1.png](https://qiita-image-store.s3.amazonaws.com/0/154355/dedf69b8-8ac3-7ebe-74ca-5f6abd7b7842.png)

#Firebase設定
Realtime Databaseを誰でも編集出来るように、ルールを変更します。

![s107.png](https://qiita-image-store.s3.amazonaws.com/0/154355/8a27b880-5d79-89d6-b34e-bb6485eafb05.png)

下記のように修正します。
`.read`と`.write`をそれぞれ`true`にします。
`.indexOn`という部分は大量にあるデータをindex化して高速にアクセスするためのものです。

```
{
  "rules": {
    ".read": true,
    ".write": true,
    "score_ranking":{
      ".indexOn": ["score"]
    }
  }
}
```

![s108.png](https://qiita-image-store.s3.amazonaws.com/0/154355/3b2784e2-2569-f8b5-7402-898eb9608b3f.png)

#XCodeに導入する
ターミナルを起動して、Podfileを作成します。

```
$ cd RankingTest/proj.ios_mac
$ pod init
```
するとproj.ios_macフォルダに`Podfile`が生成されます。
viコマンドでPodfileを編集します。

```
$ vi Podfile

platform :ios, '8.0'
target 'RankingTest-mobile' do
     pod 'Firebase'
     pod 'Firebase/Database'
end
```
Podfileを編集後、podをインストールしましょう。
この時、開いているXCodeは閉じておきます。

```
$ pod install
```
インストールが完了すると、`RankingTest.xcworkspace`ファイルが作成されます。
これを開いてみましょう。今後はこちらを起動します。
Build Settingsにある`Other Linker Flags`を編集して`$(inherited)`を追加します。
![s110.png](https://qiita-image-store.s3.amazonaws.com/0/154355/5b87747a-090e-4e75-f140-98beed8ae2cd.png)

次に`Library Search Paths`も同様に`$(inherited)`を追加します。
![s111.png](https://qiita-image-store.s3.amazonaws.com/0/154355/8dd3a831-89c2-3784-8d3d-ae8957f2ff45.png)

このままビルドするとエラーが出てしまうので、
足りないFrameworkを追加します。
![s112.png](https://qiita-image-store.s3.amazonaws.com/0/154355/42db4d20-464b-2cfa-fa33-32ab69212442.png)

GameController.frameworkを追加します。
![s113.png](https://qiita-image-store.s3.amazonaws.com/0/154355/5d9f1c3f-b7de-dcf8-73a5-932a58f27e22.png)

これで一度ビルドを行います。

#Firebase初期化
ここからFirebaseに関するプログラムを記述していきます。
iOSフォルダにある`AppController.mm`を開きましょう。

![s120.png](https://qiita-image-store.s3.amazonaws.com/0/154355/0dcfb0e0-20b6-c6b4-0c20-33f680521864.png)

```objc:AppController.mm
// Firebaseをインポート
#import "Firebase.h"

// 省略 //

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // 省略 //

    // Firebase初期化
    [FIRApp configure];

}
```

#ユーザークラス作成
ユーザークラスを作成します。Firebaseフォルダを作成し、`FIBUserBridge.h`を新規作成しましょう。
![s130.png](https://qiita-image-store.s3.amazonaws.com/0/154355/6c819a30-31dd-aa5b-b351-2391f27d1efb.png)

```objc:FIBUserBridge.h
#include "cocos2d.h"

USING_NS_CC;

class FIBUserBridge;

// コールバック用
using FIBUserBridgeCallback = std::function<void (FIBUserBridge *, const std::string &)>;

class FIBUserBridge
{
public:
    std::string userID;         // ユーザーID
    std::string userName;       // ユーザー名
    long long createTM;         // 作成日時
    
    static FIBUserBridge* userBridge(const void* user);
    static FIBUserBridge* create(const std::string& userID, const std::string& userName);
    
};
```
次に、iOSフォルダにFirebaseフォルダを追加し以下の３ファイルを新規作成しましょう。
拡張子は`.mm`という事に注意してください。

※ .mmファイルになるとC++言語を書くことが出来ます。

- FIBUserBridge.mm
- FIBUser.mm
- FUBUser.h

![s131.png](https://qiita-image-store.s3.amazonaws.com/0/154355/68e30958-5ea5-9392-f382-d599f4d97907.png)

```objc++:FIBUserBridge.mm
#import "FIBUserBridge.h"
#import "FIBUser.h"

// FIBUserからFIBUserBridgeに変換します
FIBUserBridge* FIBUserBridge::userBridge(const void *user)
{
    FIBUser *aUser = (FIBUser *)user;
    FIBUserBridge* userBridge = new FIBUserBridge();
    
    userBridge->userID = aUser.userID ? [aUser.userID UTF8String] : "";
    userBridge->userName = aUser.userName ? [aUser.userName UTF8String] : "";
    userBridge->createTM = [aUser.createTM longLongValue];
    return userBridge;
}

FIBUserBridge* FIBUserBridge::create(const std::string &userID, const std::string &userName)
{
    FIBUserBridge* userBridge = new FIBUserBridge();
    userBridge->userID = userID;
    userBridge->userName = userName;
    userBridge->createTM = 0;
    
    return userBridge;
}
```

```objc:FIBUser.mm
#import "FIBUser.h"

@implementation FIBUser

-(instancetype) initWithUserID:(NSString *)userID
                      userName:(NSString *)userName
                      createTM:(NSString *)createTM
{
    if ((self = [super init])) {
        _userID = [userID copy];
        _userName = [userName copy];
        _createTM = [createTM copy];
    }
    
    return self;
}

@end
```

```objc:FIBUser.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

@class FIBUser;

typedef void (^FIBUserCompletionHandler)(FIBUser *user, NSError *error);

@interface FIBUser : NSObject

@property (nonatomic, readonly) NSString *userID;     // ユーザーID
@property (nonatomic, readonly) NSString *userName;   // ユーザー名
@property (nonatomic, readonly) NSString *createTM;   // 作成日時

-(instancetype) initWithUserID:(NSString *)userID
                      userName:(NSString *)userName
                      createTM:(NSString *)createTM;

@end 
```

#アカウントクラス作成
次にアカウントクラスを作成します。
ClassesフォルダのFirebaseフォルダに`FIBAccountBridge.h`を作成しましょう。

![s132.png](https://qiita-image-store.s3.amazonaws.com/0/154355/55f28218-5006-a3ac-8134-fb66b555b800.png)

```objc:FIBAccountBridge.h
#include "cocos2d.h"
#include "FIBUserBridge.h"

USING_NS_CC;

class FIBAccountBridge
{
public:
    static void loginUser(const std::string& userID, const FIBUserBridgeCallback callback);
    static void signupUser(FIBUserBridge* user, const FIBUserBridgeCallback callback);
    static void updateUser(FIBUserBridge* user, const FIBUserBridgeCallback callback);
};
```
次にiOSフォルダのFirebaseフォルダに下記を新規作成しましょう。
こちらも拡張子が`.mm`なので気をつけましょう。

- FIBAccountBridge.mm
- FIBAccount.mm
- FIBAccount.h

![s133.png](https://qiita-image-store.s3.amazonaws.com/0/154355/3e7b9b11-9dd7-919a-c4d6-6f33e0406bbd.png)

```objc:FIBAccountBridge.mm
#import "FIBAccountBridge.h"
#import "FIBAccount.h"
#import "FIBUser.h"

// ユーザー取得
void FIBAccountBridge::loginUser(const std::string &userID, const FIBUserBridgeCallback callback)
{
    NSString *aUserID = [NSString stringWithCString:userID.c_str()
                                           encoding:[NSString defaultCStringEncoding]];
    
    [FIBAccount loginUserWithCompletion:aUserID completion:^(FIBUser *user, NSError *error) {
        if (user) {
            FIBUserBridge* userBridge = FIBUserBridge::userBridge(user);
            callback(userBridge, "");
        } else {
            callback(nullptr, "error");
        }
    }];
}

// ユーザー作成
void FIBAccountBridge::signupUser(FIBUserBridge *user, const FIBUserBridgeCallback callback)
{
    NSString* aUserID = [NSString stringWithCString:user->userID.c_str() encoding:[NSString defaultCStringEncoding]];
    NSString* aUserName = [NSString stringWithCString:user->userName.c_str() encoding:NSUTF8StringEncoding];
    
    FIBUser* aUser = [[FIBUser alloc] initWithUserID:aUserID
                                            userName:aUserName
                                            createTM:0];
    [FIBAccount singupUserWithCompletion:aUser completion:^(FIBUser *user, NSError *error) {
        if (user) {
            FIBUserBridge* userBridge = FIBUserBridge::userBridge(user);
            callback(userBridge, "");
            
        } else {
            callback(nullptr, "error");
            
        }
    }];
    
}

// ユーザー更新
void FIBAccountBridge::updateUser(FIBUserBridge *user, const FIBUserBridgeCallback callback)
{
    NSString* aUserID = [NSString stringWithCString:user->userID.c_str() encoding:[NSString defaultCStringEncoding]];
    NSString* aUserName = [NSString stringWithCString:user->userName.c_str() encoding:NSUTF8StringEncoding];
    
    FIBUser* aUser = [[FIBUser alloc] initWithUserID:aUserID
                                            userName:aUserName
                                            createTM:0];
    [FIBAccount singupUserWithCompletion:aUser completion:^(FIBUser *user, NSError *error) {
        if (user) {
            FIBUserBridge* userBridge = FIBUserBridge::userBridge(user);
            callback(userBridge, "");
            
        } else {
            callback(nullptr, "error");
            
        }
    }];
    
}
```

```objc:FIBAccount.mm
#import "FIBAccount.h"
#import "Firebase.h"
#import "FIBUser.h"

@implementation FIBAccount

// ログイン処理
+(void) loginUserWithCompletion:(NSString *)userID completion:(FIBUserCompletionHandler)completion
{
    // 対象ユーザーIDの情報を取得する
    FIRDatabaseReference* ref = [[FIRDatabase database] reference].ref;
    
    [[ref child:userID] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot)
    {

        // snapshotに取得したデータが入っている
        NSDictionary *snapshotData = snapshot.value;
        
        NSString* aUserID = [snapshot key];
        NSString* aUserName = [snapshotData valueForKey:@"name"];
        NSString* aCreateTM = [snapshotData valueForKey:@"createTM"];
    
        // FIBUserに格納する
        FIBUser* fibUser = [[FIBUser alloc] initWithUserID:aUserID
                                                  userName:aUserName
                                                  createTM:aCreateTM];
        completion(fibUser, nil);
    }];
}

// ユーザー登録処理
+(void) singupUserWithCompletion:(FIBUser *)fibUser completion:(FIBUserCompletionHandler)completion
{
    FIRDatabaseReference* ref = [[FIRDatabase database] reference].ref;
    [[[ref child:@"users"] child:fibUser.userID] setValue:@{@"name": fibUser.userName,
                                                            @"createTM": [FIRServerValue timestamp]}
                                      withCompletionBlock:^(NSError * _Nullable error, FIRDatabaseReference * _Nonnull ref)
                                      {
                                          if (error != nil) {
                                              completion(nil, error);
                                          } else {
                                              // 登録完了後、Firebaseからデータを取得する
                                              [[[[[FIRDatabase database] reference].ref child:@"users"] child:fibUser.userID] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) {
                                                  NSDictionary * data = snapshot.value;
                                                  FIBUser* wkUser = [[FIBUser alloc] initWithUserID:snapshot.key
                                                                                           userName:[data valueForKey:@"name"]
                                                                                           createTM:[data valueForKey:@"createTM"]];
                                                  completion(wkUser, nil);
                                              }];
                                          }
                                      }];
    }

// ユーザー更新
+(void) updateUserWithCompletion:(FIBUser *)fibUser completion:(FIBUserCompletionHandler)completion
{
    FIRDatabaseReference* ref = [[FIRDatabase database] reference].ref;
    [[[ref child:@"users"] child:fibUser.userID] updateChildValues:@{@"name": fibUser.userName}
                                               withCompletionBlock:^(NSError * _Nullable error, FIRDatabaseReference * _Nonnull ref) {
                                                   if (error != nil) {
                                                       completion(nil, error);
                                                   } else {
                                                       [[[[[FIRDatabase database] reference].ref child:@"users"] child:fibUser.userID] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) {
                                                           NSDictionary * data = snapshot.value;
                                                           FIBUser* wkUser = [[FIBUser alloc] initWithUserID:snapshot.key
                                                                                                    userName:[data valueForKey:@"name"]
                                                                                                    createTM:[data valueForKey:@"createTM"]];
                                                           completion(wkUser, nil);
                                                       }];
                                                   }
                                               }];
}
@end
```
```objc:FIBAccount.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "FIBUser.h"

@interface FIBAccount : NSObject

+(void) loginUserWithCompletion:(NSString * )userID
                     completion:(FIBUserCompletionHandler)completion;
+(void) singupUserWithCompletion:(FIBUser *)fibUser
                      completion:(FIBUserCompletionHandler)completion;
+(void) updateUserWithCompletion:(FIBUser *)fibUser
                      completion:(FIBUserCompletionHandler)completion;
@end
```
#GSUtilComクラスを作成する
共通関数を作成しましょう。
ここでは、端末固有のIDを取得する処理を記述します。
このIDがユーザーIDとなります。

![s140.png](https://qiita-image-store.s3.amazonaws.com/0/154355/d3b881e7-b3df-8531-79fd-343f8a178638.png)

```objc:GSUtilCom.h
#include <string.h>
#include "cocos2d.h"

using namespace std;
using namespace cocos2d;

class GSUtilCom
{
public:
    // ユーザーID
    static std::string getUserID();
};

```
次に`GSUtilCom.mm`ファイルを新規作成します。

![s141.png](https://qiita-image-store.s3.amazonaws.com/0/154355/6fc09a3c-009c-8cb6-3522-ce91ac0a1026.png)

```objc:GSUtilCom.mm
#import "GSUtilCom.h"

// ユーザーID取得
std::string GSUtilCom::getUserID()
{
    // 端末固有のIDを取得する
    NSString* userID = [[UIDevice currentDevice].identifierForVendor UUIDString];
    
    return [userID UTF8String];
}
```
#ユーザー登録処理を記述する
実際の処理を`HelloWorldScene.cpp`に記述しましょう

![s150.png](https://qiita-image-store.s3.amazonaws.com/0/154355/e9e107de-b5f8-5b5a-1071-66fc1d3f6495.png)


```cpp:HelloWorldScene.cpp
#include "HelloWorldScene.h"
#include "GSUtilCom.h"
#include "FIBUserBridge.h"
#include "FIBAccountBridge.h"

#define WINSIZE Director::getInstance()->getWinSize()
#define WINCENTER Point(WINSIZE.width*0.5, WINSIZE.height*0.5)

Scene* HelloWorld::createScene()
{
    // 'scene' is an autorelease object
    auto scene = Scene::create();
    
    // 'layer' is an autorelease object
    auto layer = HelloWorld::create();

    // add layer as a child to scene
    scene->addChild(layer);

    // return the scene
    return scene;
}

// on "init" you need to initialize your instance
bool HelloWorld::init()
{
    //////////////////////////////
    // 1. super init first
    if ( !Layer::init() )
    {
        return false;
    }
    
    // セットアップ
    this->setup();
    
    return true;
}

// セットアップ処理
void HelloWorld::setup()
{
    // ユーザー作成
    Sprite* btnCreateUser_ON = Sprite::create();
    btnCreateUser_ON->setTextureRect(Rect(0, 0, 200, 100));
    btnCreateUser_ON->setColor(Color3B::GRAY);
    
    Sprite* btnCreateUser_OFF = Sprite::create();
    btnCreateUser_OFF->setTextureRect(Rect(0, 0, 200, 100));
    btnCreateUser_OFF->setColor(Color3B(0,0,200));
    
    MenuItemSprite* btnCreateUser = MenuItemSprite::create(btnCreateUser_OFF,
                                                           btnCreateUser_ON,
                                                           [this](Ref* sender) {this->createUserTapped((MenuItemSprite*)sender);});
    Label* lblCreateUser = Label::createWithSystemFont("ユーザー作成", "arial", 26);
    lblCreateUser->enableOutline(Color4B::BLACK, 1);
    lblCreateUser->setPosition(Vec2(btnCreateUser->getContentSize().width*0.5, btnCreateUser->getContentSize().height*0.5));
    btnCreateUser->addChild(lblCreateUser);
    
    Menu* menu = Menu::create(btnCreateUser, NULL);
    menu->setPosition(Vec2(WINCENTER.x, WINSIZE.height*0.9));
    this->addChild(menu);
}

// ユーザー作成
void HelloWorld::createUserTapped(cocos2d::MenuItemSprite *menuSpr)
{
    // ユーザーIDを取得
    std::string aUserID = GSUtilCom::getUserID();
    std::string userName = "テスト";
    FIBUserBridge* fibUser = FIBUserBridge::create(aUserID, userName);

    // ユーザー登録処理
    FIBAccountBridge::signupUser(fibUser, [=](FIBUserBridge* user, std::string error)
    {
        if (error.length()) {
            log("登録失敗=%s", error.c_str());
        } else {
            if (user) {
                log("登録しました！");
                log("id=%s", user->userID.c_str());
                log("name=%s", user->userName.c_str());
                log("createTM=%lld", user->createTM);

                // データ保持
                _userID = user->userID;
            }
        }
    });

}
```
```c:HelloWorldScene.h
#include "cocos2d.h"

USING_NS_CC;

class HelloWorld : public cocos2d::Layer
{
public:
    static cocos2d::Scene* createScene();

    virtual bool init();
    
    // implement the "static create()" method manually
    CREATE_FUNC(HelloWorld);
    
private:
    // ユーザーID保持
    CC_SYNTHESIZE(std::string, _userID, UserID);

    // セットアップ
    void setup();
    
    // ユーザー作成ボタンタップ処理
    void createUserTapped(MenuItemSprite* menuSpr);
};

```

これでビルドして実行すると、下記のような画面が表示されます。
ユーザー登録ボタンをタップすると、Firebase上にユーザーが書き込まれます。

![s160.png](https://qiita-image-store.s3.amazonaws.com/0/154355/3dc6f167-e97e-76eb-7fc0-6063dc4af5bf.png)

![s161.png](https://qiita-image-store.s3.amazonaws.com/0/154355/66a9b6d8-99b3-4026-da6d-5ac11e07e9b2.png)

[次回](http://qiita.com/h-takauma/items/42c898a548e663306f4a)はスコア送信処理を実装します。