cocos2d-xでsocket.io(第五回cocos2d-x勉強会)

先日、開催されました、
[【#TechBuzz】第5回cocos2d-x勉強会](http://atnd.org/events/50189)
で登壇されました、
トランスリミット高場さん、工藤さん
全世界と戦える“脳トレ”アプリ「[BrainWars](http://translimit.co.jp/services/brainwars.html)」
で話された内容と同じになります。

#cocos2d-xでsocket.io
cocos2d-xでは[socket.io](http://socket.io/)を利用することが出来ます。
その技術を利用して作られたゲームが[BrainWars](http://translimit.co.jp/services/brainwars.html)とのこと。
このお話を聞いて、自分でもやってみました。
その際に直面した問題や、当日お話された問題点を共有出来ればと思います。

実装内容としては、クライアントから送信した文字列をオウム返しで、
socketサーバが返却するだけのシンプルな構成となっております。

cocos2d-x v3.1.1
nodejs v0.10.29
socket.io v0.9.9

で実装しています。


#クライアント側実装(cocos2d-x)

```cpp:HelloWorldScene.h

#ifndef __HELLOWORLD_SCENE_H__
#define __HELLOWORLD_SCENE_H__

#include "cocos2d.h"
#include <SocketIO.h>
#include "ui/cocosGUI.h"

using namespace cocos2d::network;
using namespace cocos2d::ui;

class HelloWorld : public cocos2d::Layer , public SocketIO::SIODelegate
{
private:
    int index;
    SIOClient* _client;
    TextField* editBox;
   
public:
    // there's no 'id' in cpp, so we recommend returning the class instance pointer
    static cocos2d::Scene* createScene();

    // socket.io eventのevent listener
    void onReceiveEvent(SIOClient* client , const std::string& data);
    
    // SIODelegate
    virtual void onConnect(SIOClient* client);
    virtual void onMessage(SIOClient* client, const std::string& data);
    virtual void onClose(SIOClient* client);
    virtual void onError(SIOClient* client, const std::string& data);
    
    // UI周り
    void textFieldEvent(Ref *pSender, TextField::EventType type);
    void addTalkPlayer(const std::string& str);
    void addTalkOther(const std::string& str);
   
   
    virtual bool init(); 
   
    // a selector callback
    void menuCloseCallback(cocos2d::Ref* pSender);
   
    // implement the "static create()" method manually
    CREATE_FUNC(HelloWorld);
};

#endif // __HELLOWORLD_SCENE_H__

```


```cpp:HelloWorldScene.cpp

#include "HelloWorldScene.h"
#include "json/rapidjson.h"
#include "json/document.h"

USING_NS_CC;

// UI関係
const static int PLAYER_TEXT_X = 900;
const static int OTHER_TEXT_X = 50;
const static int TEXT_H = 60;

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()
{
    index = 0;

    // 略
   
    editBox = TextField::create("please input text" , "Meiryo" , 40);
    editBox->setContentSize(Size(visibleSize.width , 50));
    editBox->setPosition(Point(visibleSize.width / 2 , 30));
    editBox->addEventListener(CC_CALLBACK_2(HelloWorld::textFieldEvent, this));
    this->addChild(editBox);
    
    // ここでsocket.io connection開始。clientを持っておく
    _client = SocketIO::connect("http://xxx.compute.amazonaws.com:3000", *this);
    _client->on("hello", CC_CALLBACK_2(HelloWorld::onReceiveEvent, this));
   
    return true;
}

void HelloWorld::onConnect(SIOClient* client){
     // SocketIO::connect success
}

void HelloWorld::onMessage(SIOClient* client, const std::string& data){
     // SocketIO::send receive
}
void HelloWorld::onClose(SIOClient* client){
    // SocketIO::disconnect success
}
void HelloWorld::onError(SIOClient* client, const std::string& data){
    // SocketIO::failed
}

/**
 * serverからのemit("hello")をここでlisten
 */
void HelloWorld::onReceiveEvent(SIOClient* client , const std::string& data){
   
    rapidjson::Document doc;
    doc.Parse<rapidjson::kParseDefaultFlags>(data.c_str());
    rapidjson::Value &val = doc["args"];
    std::string value = val[rapidjson::SizeType(0)]["value"].GetString();
   
    addTalkOther(value);
};

//------------------------------------------------------
// ここからUI周り

/**
 * textFieldの処理
 */
void HelloWorld::textFieldEvent(Ref *pSender, TextField::EventType type)
{
    TextField* text;
    std::string sendText;
    switch (type)
    {
        // IMEが閉じた時 
        case TextField::EventType::DETACH_WITH_IME:
            text = (TextField*)pSender;
           
            sendText = "[{\"value\":\"" + text->getStringValue() + "\"}]";
            _client->emit("hello", sendText);
            addTalkPlayer(text->getStringValue());
            break;
        default:
            break;
    }
}

/**
 * プレイヤーUI
 */
void HelloWorld::addTalkPlayer(const std::string& str){
    Size size = Director::getInstance()->getVisibleSize();
   
    DrawNode* draw = DrawNode::create();
   
    int originalX = PLAYER_TEXT_X;
    int originalY = size.height - (TEXT_H * (index + 1));
   
    int x = originalX - 290;
    int y = originalY - 60;
    int w = 300;
    int h = 60;
   
    Vec2 points[] = {
        Vec2(x , y),
        Vec2(x + w , y),
        Vec2(x + w , y + h),
        Vec2(x , y + h),
    };
   
    this->addChild(draw);
    draw->drawPolygon(points, 4, Color4F(0 , 0.5, 0, 1), 1, Color4F(0,0,1,1));
   
    auto text = Text::create(str, "Meiryo", 40);
    text->setTextHorizontalAlignment(TextHAlignment::RIGHT);
    text->setAnchorPoint(Point(1.0 , 1.0));
    text->setPosition(Point(originalX , originalY));
   
    this->addChild(text);
    index++;
}

/**
 * その他UI
 */
void HelloWorld::addTalkOther(const std::string& str){
    Size size = Director::getInstance()->getVisibleSize();
   
    DrawNode* draw = DrawNode::create();
   
    int originalX = OTHER_TEXT_X;
    int originalY = size.height - (TEXT_H * (index + 1));
   
    int x = originalX - 10;
    int y = originalY - 60;
    int w = 300;
    int h = 60;
   
    Vec2 points[] = {
        Vec2(x , y),
        Vec2(x + w , y),
        Vec2(x + w , y + h),
        Vec2(x , y + h),
    };
   
    this->addChild(draw);
    draw->drawPolygon(points, 4, Color4F(0.5, 0, 0, 1), 1, Color4F(1,0,0,1));
   
    auto text = Text::create(str, "Meiryo", 40);
    text->setTextHorizontalAlignment(TextHAlignment::LEFT);
    text->setAnchorPoint(Point(0.0 , 1.0));
    text->setPosition(Point(originalX , originalY));
    text->setColor(Color3B(255, 255, 0));
    this->addChild(text);
    index++;
}


void HelloWorld::menuCloseCallback(Ref* pSender){ // 略 }

```

クライアント側はこのような実装になります。
SIODelegateを実装する事が最小限必須です。
本当はrapidjsonも使わずに実装したかったのですが、
パース部分がめんどくさかったので、使ってしまいました…。

#サーバ側実装(nodejs+socket.io)

```js:socket.js

server = require('http').Server();
var socketIO = require('socket.io');
var io = socketIO.listen(server);
io.sockets.on('connection', function(socket){
  console.log("connect");
  socket.on('hello', function(data){
      console.log("hello : " + data.value);
      io.sockets.emit('hello', { value: data.value });
  });
  socket.on('disconnect', function(){
      console.log("disconnect");
  });
  io.sockets.emit('hello', { value: "welcome" });
});
server.listen(3000);

```

実サービスにするならばexpressを利用して
プロジェクトとしてしっかりと管理するのが良いと思いますが、
今回はサンプルですので、最小限で。

#実行

クライアント
![スクリーンショット 2014-06-23 14.52.24.png](https://qiita-image-store.s3.amazonaws.com/0/26453/fa899351-dfe2-8a7e-771b-1001a0f0ba8a.png)

サーバ
![socketio_log.png](https://qiita-image-store.s3.amazonaws.com/0/26453/5a8f16c6-29e3-c471-5eff-a74ebb06cf04.png)


まず起動するとwelcomeメッセージを受信します。
画面下部にある入力フォームに文言を入力すると、socket.ioを介してサーバに送信され、
サーバはそのイベントを受け取り、オウム返しで同じメッセージをクライアントに返却しています。

#注意点やハマりどころ

##json
ここはsocket.ioの基本かもしれないのですが、
基本的にjsonp_pollingになりますので、
受け渡しするパラメータがjson形式で無いと上手く送信されません。
例えば、

```cpp:HelloWorldScene.cpp

_client->emit("event" , "aaaaa");

```

このように記述すると、送信しました！というログは出るのですが、
socketサーバ側では、受け取りました！というログが出ないという現象になります。

##通信量
トランスリミットの工藤さんが登壇時に話された内容です。
cocos2d-xのsocket.ioは内部的にはwebSocketになっています。
そして、一度の通信では"2KB"前後しかやりとりすることが出来ず、
それ以上になると2度の通信に分割されてしまうそうです。

通信量を減らすのが最も良いですが、どうしてもそのように出来ない場合は
分割を判定したり、結合したりする仕組みが必要になります。

##socket.ioのバージョン
現在は1.0.4までバージョンアップしています。
が、最新バージョンを使用すると、上手く行かなくなります。

**URL + /socket.io/1**

というような通信の結果が404となってしまいます。
これを回避するには、

**transport=polling**

というパラメータを付加するという手があるのですが、
これを付加してもcocos2d-x側はどうも上手く行きません。
(connectionは貼れるが、Heart Beatに失敗して、即切断される)

なので、socket.ioはv0.9.x以下を利用するのがオススメです。

##node.js
make長いです！
(すっかり忘れてました)

##spine/Json.hを使わない
パース出来るがエンコード出来なかったりするようです。
[picoJson](https://github.com/kazuho/picojson)や[rapidJson](https://github.com/miloyip/rapidjson)
オススメです。


##32bitと64bitの違いによる動きの違い
cocos2d-xは32bit系と64bit系で実装が違うため、
32bitではソケットが繋がるが、
64bitではソケットが繋がらない
というcocos2d-xのVersionがあるようです。
最新のcocos2d-xを利用するか、修正差分の反映が必要となります。