先日、開催されました、
【#TechBuzz】第5回cocos2d-x勉強会
で登壇されました、
トランスリミット高場さん、工藤さん
全世界と戦える“脳トレ”アプリ「BrainWars」
で話された内容と同じになります。
#cocos2d-xでsocket.io
cocos2d-xではsocket.ioを利用することが出来ます。
その技術を利用して作られたゲームがBrainWarsとのこと。
このお話を聞いて、自分でもやってみました。
その際に直面した問題や、当日お話された問題点を共有出来ればと思います。
実装内容としては、クライアントから送信した文字列をオウム返しで、
socketサーバが返却するだけのシンプルな構成となっております。
cocos2d-x v3.1.1
nodejs v0.10.29
socket.io v0.9.9
で実装しています。
#クライアント側実装(cocos2d-x)
#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__
#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)
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を利用して
プロジェクトとしてしっかりと管理するのが良いと思いますが、
今回はサンプルですので、最小限で。
#実行
まず起動するとwelcomeメッセージを受信します。
画面下部にある入力フォームに文言を入力すると、socket.ioを介してサーバに送信され、
サーバはそのイベントを受け取り、オウム返しで同じメッセージをクライアントに返却しています。
#注意点やハマりどころ
##json
ここはsocket.ioの基本かもしれないのですが、
基本的にjsonp_pollingになりますので、
受け渡しするパラメータがjson形式で無いと上手く送信されません。
例えば、
_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やrapidJson
オススメです。
##32bitと64bitの違いによる動きの違い
cocos2d-xは32bit系と64bit系で実装が違うため、
32bitではソケットが繋がるが、
64bitではソケットが繋がらない
というcocos2d-xのVersionがあるようです。
最新のcocos2d-xを利用するか、修正差分の反映が必要となります。