WonderPlanet DEVELOPER BLOG

ワンダープラネットの開発者ブログです。モバイルゲーム開発情報を発信。

Cocos2d-xのゲームを遠隔操作してみよう

今回のエンジニアブログ担当の岩原です。
今回はCocos2d-xに備わっているとある機能をご紹介します。

Hot Updating

Cocos2d-x+Luaには、Hot Updatingという仕組みが用意されています。
これはコードを変更すると、自動的に更新されてアプリにも反映されるという仕組みです。
Cocos Code IDEで主に利用されいる仕組みだったと思います。
大体の処理シーケンスは以下のとおりだと思います。

  1. ゲームが立ち上がると同時にTCPサーバーとファイルサーバーを立ち上げ、接続を待ち受ける。
  2. クライアント側(Cocos Code IDEなど)からTCPでコマンドを送信する。ファイルの場合はファイルも合わせて送信する。
  3. ゲームではそれを受け取り、処理を行い、結果をクライアント側に返す。

このHot Updatingですが、一部機能はC++からも利用可能です。
早速使ってみましょう。

用意するもの

・新規Cocos2d-x v3.x C++プロジェクト(サンプルコードはv3.2を使用)
・TCPクライアント
・開発マシンと通信できる端末とそのIPアドレス。

概要

ファイルの送受信は使用できませんが、コマンドの実行は可能です。
実行可能なコマンドは予めいくつか用意されており、さらに自分でコマンドを追加することも出来ます。
コマンドの追加は次回にかけたら書きたいと思います。
コマンドの機能はConsoleクラスにまとまっています。気になる方はコードを読んでみるとよいでしょう。

今回はいくつか用意されているコマンドから、「touch」コマンドを遠隔で実行することにします。
touchコマンドはtapとswipeの2種類あるのですが、今回はtapを行います。

C++で書いてみる

デフォルトのシーンからあの例の画像を表示している箇所をコメントアウトし、ラベルのみを表示するようにします。
また、タッチして動かしている間とタッチを離した時の座標をラベルに表示するようにしましょう。

// on "init" you need to initialize your instance  
bool HelloWorld::init()  
{  
    //省略  
  
    // add the label as a child to this layer  
    this->addChild(label, 1);  
    //ここから追加  
    // touch event listener create.  
    auto listener = EventListenerTouchOneByOne::create();  
    listener->onTouchBegan = [=](Touch* touch, Event* event) -> bool {  
        return true;  
    };  
    listener->onTouchMoved = [=](Touch* touch, Event* event) -> void {  
        char msg[128];  
        snprintf(msg, sizeof(msg), "x[%f] y[%f]",touch->getLocationInView().x,touch->getLocationInView().y);  
        label->setString(msg);  
        CCLOG("move %s", msg);  
    };  
    listener->onTouchEnded = [=](Touch* touch, Event* event) -> void {  
        char msg[128];  
        snprintf(msg, sizeof(msg), "x[%f] y[%f]",touch->getLocationInView().x,touch->getLocationInView().y);  
        label->setString(msg);  
        CCLOG("end %s", msg);  
    };  
      
    this->getEventDispatcher()->addEventListenerWithSceneGraphPriority (listener, this);  
    //ここまで追加  
  
    //省略  
}  

次に、TCPサーバーの立ち上げを行います。
全体で行う場合はAppDelegate.cppのapplicationDidFinishLaunching関数、
シーン限定でやる場合は適用したいシーンのinit関数で以下のようなコードを書くとよいでしょう。
今回はシーン限定で書く想定で行こうと思います。

bool HelloWorld::init()  
{  
    //////////////////////////////  
    // 1. super init first  
    if ( !Layer::init() )  
    {  
        return false;  
    }  
    //ここから追加  
#ifdef COCOS2D_DEBUG  
    //TCPサーバーを立ち上げ、接続要求を待ち受ける  
    Director::getInstance()->getConsole()->listenOnTCP(6010);  
#endif  
    //ここまで追加  
  
    //以下省略  

6010はポート番号になります。
空いているポートであればいくつでも良いと思いますが、Cocos2d-x+Luaで使用しているポートが6010だったので、
それに合わせています。

シーン単位であれば、onExit辺りでTCPサーバーを停止するのを忘れないようにしましょう。

void HelloWorld::onExit()  
{  
#ifdef COCOS2D_DEBUG  
    //TCPサーバーを停止する  
    Director::getInstance()->getConsole()->stop();  
#endif  
}  

全体のコードは以下のとおりです。

#include "HelloWorldScene.h"  
  
USING_NS_CC;  
  
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;  
    }  
      
#ifdef COCOS2D_DEBUG  
    //TCPサーバーを立ち上げ、接続要求を待ち受ける  
    Director::getInstance()->getConsole()->listenOnTCP(6010);  
#endif  
      
    Size visibleSize = Director::getInstance()->getVisibleSize();  
    Vec2 origin = Director::getInstance()->getVisibleOrigin();  
  
    /////////////////////////////  
    // 2. add a menu item with "X" image, which is clicked to quit the program  
    //    you may modify it.  
  
    // add a "close" icon to exit the progress. it's an autorelease object  
    auto closeItem = MenuItemImage::create(  
                                           "CloseNormal.png",  
                                           "CloseSelected.png",  
                                           CC_CALLBACK_1(HelloWorld::menuCloseCallback, this));  
      
    closeItem->setPosition(Vec2(origin.x + visibleSize.width - closeItem->getContentSize().width/2 ,  
                                origin.y + closeItem->getContentSize().height/2));  
  
    // create menu, it's an autorelease object  
    auto menu = Menu::create(closeItem, NULL);  
    menu->setPosition(Vec2::ZERO);  
    this->addChild(menu, 1);  
  
    /////////////////////////////  
    // 3. add your codes below...  
  
    // add a label shows "Hello World"  
    // create and initialize a label  
      
    auto label = Label::createWithSystemFont("Hello World", "Arial", 24);  
      
    // position the label on the center of the screen  
    label->setPosition(Vec2(origin.x + visibleSize.width/2,  
                            origin.y + visibleSize.height - label->getContentSize().height));  
  
    // add the label as a child to this layer  
    this->addChild(label, 1);  
      
    // touch event listener create.  
    auto listener = EventListenerTouchOneByOne::create();  
    listener->onTouchBegan = [=](Touch* touch, Event* event) -> bool {  
        return true;  
    };  
    listener->onTouchMoved = [=](Touch* touch, Event* event) -> void {  
        char msg[128];  
        snprintf(msg, sizeof(msg), "x[%f] y[%f]",touch->getLocationInView().x,touch->getLocationInView().y);  
        label->setString(msg);  
        CCLOG("move %s", msg);  
    };  
    listener->onTouchEnded = [=](Touch* touch, Event* event) -> void {  
        char msg[128];  
        snprintf(msg, sizeof(msg), "x[%f] y[%f]",touch->getLocationInView().x,touch->getLocationInView().y);  
        label->setString(msg);  
        CCLOG("end %s", msg);  
    };  
      
    this->getEventDispatcher()->addEventListenerWithSceneGraphPriority (listener, this);  
  
    // add "HelloWorld" splash screen"  
    //auto sprite = Sprite::create("HelloWorld.png");  
  
    // position the sprite on the center of the screen  
    //sprite->setPosition(Vec2(visibleSize.width/2 + origin.x, visibleSize.height/2 + origin.y));  
  
    // add the sprite as a child to this layer  
    //this->addChild(sprite, 0);  
      
    return true;  
}  
  
void HelloWorld::onExit()  
{  
#ifdef COCOS2D_DEBUG  
    //TCPサーバーを停止する  
    Director::getInstance()->getConsole()->stop();  
#endif  
}  
  
  
void HelloWorld::menuCloseCallback(Ref* pSender)  
{  
#if (CC_TARGET_PLATFORM == CC_PLATFORM_WP8) || (CC_TARGET_PLATFORM == CC_PLATFORM_WINRT)  
    MessageBox("You pressed the close button. Windows Store Apps do not implement a close button.","Alert");  
    return;  
#endif  
  
    Director::getInstance()->end();  
  
#if (CC_TARGET_PLATFORM == CC_PLATFORM_IOS)  
    exit(0);  
#endif  
}  

本番アプリにも立ち上がると大変なので、必ず#ifdef COCOS2D_DEBUGで囲むようにしましょう。
これで対象のシーンを表示した時のみTCPサーバーが立ち上がり、接続を待ち受けるようになります。
アプリをビルド&実行しましょう。

TCPクライアントを使用し、コマンドの送受信を行う

アプリを立ち上げた状態で、事前に調べたIPアドレスとポート番号を使用し、TCP接続を行います。
無事に接続できると、

>  

と表示されていると思います。

次にコマンドの送信を行うのですが、いくつか注意点があります。
コマンドの切れ目を\nで判断しているため、\nを末尾に付けないと正しく認識されません。
気をつけましょう。
また、アプリ側から送られてくる応答データが途中で切れ、次回の応答の際にまとめて返ってくることが多々あります。

では、さっそくtouchコマンドを使用してタップを行いましょう。
コマンドは以下のとおりです。

touch tap x座標 y座標  

ここでのx座標、y座標はsetPositionで指定する座標と同じです。
たとえば、x座標に100、y座標に200を指定する場合は

touch tap 100 200  

というようにしています。

このコマンド文字列をアプリに送信すると、
ラベルにそれぞれの座標が表示されたと思います。

画面をタップしても同じ結果が得られると思います。

どのようなコマンドがあるか見てみる

helpと打つと、現在使用できるコマンドとその説明が表示されます。
結構な頻度でぶった切られるので、2回打つと良いでしょう。
デフォルトではこれだけ用意されています。

Available commands:  
    config          Print the Configuration object  
    debugmsg        Whether or not to forward the debug messages on the console. Args: [on | off]  
    director        director commands, type -h or [director help] to list supported directives  
    exit            Close connection to the console  
    fileutils       Flush or print the FileUtils info. Args: [flush | ]   
    fps         Turn on / off the FPS. Args: [on | off]   
    help            Print this message  
    projection      Change or print the current projection. Args: [2d | 3d]  
    resolution      Change or print the window resolution. Args: [width height resolution_policy | ]  
    scenegraph      Print the scene graph  
    texture         Flush or print the TextureCache info. Args: [flush | ]   
    touch           simulate touch event via console, type -h or [touch help] to list supported directives  
    upload          upload file. Args: [filename base64_encoded_data]  
    version         print version string   

忘れていなければ、次回はコマンドの追加を行いたいと思います。