2014.10.08

cocos2d-xでPhotonを使ってみよう

こんにちは。GMOインターネットグループのゲーム開発の技術支援をしているF.Sです。
本エントリでは、Photonネットワークエンジン を cocos2d-x から使う手順を紹介します。

Photonネットワークエンジンはスクウェア・エニックス様の「聖剣伝説 RISE of MANA」の
リアルタイムバトルにも導入されていることで注目を浴びていますね。
Photon Unity Networking framework の提供によって Unity からの利用は容易なようですが、
ここではあえて cocos2d-x でチャレンジしてみようと思います。


Photonとは

リアルタイム、マルチプレイヤーゲームを可能にする低レイテンシーなネットワークエンジンで、
米ExitGames社が開発しています。

Photon Realtime ホーム
https://www.exitgames.com/ja/Realtime

利用にあたり、Photon Server(オンプレミス)とPhoton Cloud(クラウド)が選択できます。
Photon Server は、サーバを自前で用意する必要がありますが、自前のゲームサーバとの通信など、
C#を使ったサーバアプリケーションのカスタマイズが可能になります。
Photon Cloud は、バックエンドを意識することなく、すぐに利用することができます。

本格的なゲームアプリケーションへの導入となると、Photon Server を選択することが多いと思いますが、
ここでは、手軽に試せるPhoton Cloudを使用する例を紹介します。
Photon Cloud は、Photon Server に同梱されているアプリケーションとほぼ同じものが稼働しているので、
必要に応じて Photon Server に移行する際でもクライアント開発の資産はそのまま活用できます。
そのため、モックアップは Photon Cloud で、本格的な開発では Photon Server で、といった使い方も想定できます。

それでは、早速やってみましょう。

なお、筆者の環境は下記の通りです。

  • MacOS X 10.9.4
  • cocos2d-x 3.0
  • Xcode 5.1.1
  • Android Developer Tool v22.6.2-1085508
 

Photon のサインアップ

まずサインアップ(無料)しましょう。必要なのはメールアドレスのみです。
サインアップすることで、20CCUまでの無料版PhotonアプリケーションとSDKが利用できます。

Photon サインアップ
https://www.exitgames.com/ja/account/signup

photon01_signup

サインアップが済んだら、割り当てられたPhoton Cloud アプリケーションのダッシュボードを見てみましょう。

Photon ダッシュボード
https://www.exitgames.com/ja/Realtime/Dashboard

photon02_dashboard

ここに記載されているアプリケーションIDが、自分に割り当てられたPhoton Cloud アプリケーションを示すIDです。
このIDはクライアントから接続する際に必要になります。

 

Photon SDKのダウンロード

こちらからクライアントSDK(v3)をダウンロードします。

Photon Realtime SDKs
https://www.exitgames.com/ja/Realtime/Download

photon03_sdk

iOSとAndroid向けにアプリを作成する想定で、下記のSDKをダウンロードします。
  • Photon-AndroidNDK_v3-2-5-3_Cloud_SDK
  • Photon-iOS_v3-2-5-3_Cloud_SDK
photon04_sdk_download

ここで、cocos2d-x プロジェクトを作成します。
$ cocos new MyPhoton -p com.example.myphoton -l cpp -d ~/Documents/cocos2d-x
ダウンロードしたSDKを、それぞれプロジェクトフォルダに格納します。
(下記例では、SDKのフォルダをリネームしています)
  • Photon-AndroidNDK_SDK
  • Photon-iOS_SDK
photon05_project

 

プロジェクトのセットアップ

SDKに含まれているデモを参考に、プロジェクトのセットアップをします。
  • iOSのセットアップ(Build Settings)
    1. 「Linking」>「Header Search Paths」に下記を追加します
      • -lCommon-cpp_$(CONFIGURATION)_$(PLATFORM_NAME)
      • -lPhoton-cpp_$(CONFIGURATION)_$(PLATFORM_NAME)
      • -lLoadBalancing-cpp_$(CONFIGURATION)_$(PLATFORM_NAME)
        photon06_ios_setting1
    2. 「Search Paths」>「Header Search Paths」に下記を追加します
      • $(SRCROOT)/Photon-iOS_SDK
        photon07_ios_setting2
    3. 「Library Paths」>「Library Search Paths」に下記を追加します
      • $(SRCROOT)/Photon-iOS_SDK/Common-cpp/lib
      • $(SRCROOT)/Photon-iOS_SDK/Photon-cpp/lib
      • $(SRCROOT)/Photon-iOS_SDK/LoadBalancing-cpp/lib
        photon08_ios_setting3
    4. PhotonSDKは現時点で64bitのアーキテクチャーが未サポートのため「Architectures」>「Valid Architectures」から
      “arm64” を削除して、「Build Active Architecture Only」を “No” にします (cocos2d_libs.xcodeprojも同様に変更します)
      photon09_ios_setting4 
      photon10_ios_setting5
  • Androidのセットアップ(proj.android/jni/Android.mk の編集)
    1. インクルードパスの追加
      • LOCAL_C_INCLUDES += $(LOCAL_PATH)/../Photon-AndroidNDK_SDK
    2. CFLAGの追加
      • LOCAL_CFLAGS := -DEG_DEBUGGER -D__STDINT_LIMITS -D_EG_ANDROID_PLATFORM
    3. ライブラリをリンク
      • LOCAL_STATIC_LIBRARIES := loadbalancing-cpp-static-prebuilt photon-cpp-static-prebuilt common-cpp-static-prebuilt
      • LOCAL_LDLIBS := -llog
    4. インポートモジュールのパスを追加
      • $(call import-add-path, $(shell pwd)/Photon-AndroidNDK_SDK/Common-cpp)
      • $(call import-add-path, $(shell pwd)/Photon-AndroidNDK_SDK/Photon-cpp)
      • $(call import-add-path, $(shell pwd)/Photon-AndroidNDK_SDK/LoadBalancing-cpp)
    5. インポートモジュールの追加
      • $(call import-module,loadbalancing-cpp-prebuilt)
        photon11_android_setting
 

サンプルアプリケーション

一旦、cocos2d-x の世界に移り、サンプルとなるアプリケーションを作成します。
cocos2d-xの素のアプリケーションにて、タップした位置にパーティクルを生成してみましょう。
HelloWorldシーンにタッチイベントリスナーを登録し、onTouchBegan でパーティクルを生成する処理を記述します。

実行結果はこんな感じです。

photon12_sample_app

このアプリケーションにて、Photonを使って同時に参加している複数の端末でタップされた場所に
パーティクルを発生させてみたいと思います。

 

Photonクライアントの作成

Photonクライアントは ExitGames::LoadBalancing::Listener を実装し、
ExitGames::LoadBalancing::Client のインスタンスにリスナーを登録して利用するのですが、
ここでは説明を簡略にするために、SDKのデモアプリケーションのクラスを流用します。

 
  1. Demos/demo_loadBalancing から、NetworkLogic.h, NetworkLogic.cpp をコピーしてプロジェクトに追加
  2. 同クラス内のOutputListener(メッセージを画面出力するために使っているクラス)を使っている箇所をすべて削除
  3. NetworkLogic.h で “LoadBalancing-cpp/inc/Client.h” を include
  4. mLoadBalancingClient 生成時のアプリケーションIDを上で発行したIDに変更
  5. mLoadBalancingClient.connect() に渡すFQDNを、”app-eu.exitgamescloud.com” に変更
  6. HelloWorldScene.h で “NetworkLogic.h” を include
  7. cocos2dのSizeとPointクラス名がPhoton SDKと重複するようなので、cocos2dの名前空間を指定
 

これでビルドが通るようにはなると思います。
かなりやっつけ感がありますが、ひとまず動くものを試してみる、ということで了承ください。

 

アプリケーションへの組み込み

上述のサンプルアプリケーションにて、Photonによる通信処理を組み込んでみたいと思います。
処理の概要は次の通りです。

 
  1. 開始時にルームに参加、ルームがなければ作成します
  2. 端末でタップされた座標を、ルームに参加している全員にイベント送信します
  3. イベントを受け取ったらプレイヤー番号と座標をイベントキューに追加します
  4. シーンの描画更新処理で、イベントキューからプレイヤー番号と座標を取り出してパーティクルを生成します
 

ソースの追加、変更箇所です。
  • NetworkLogic.h にクラス変数、メソッドを追加
    #include <array>
    #include <queue>
    public:
        // ルームが存在するか否かを返すメソッド
        bool isRoomExists(void);
        // イベントを送信するメソッド
        void sendEvent(nByte code, ExitGames::Common::Hashtable* eventContent);
    
        // 自分のプレイヤー番号
        int playerNr = 0;
        // イベントキュー
        std::queue&lt;std::array&lt;float, 3&gt;&gt; eventQueue;
  • NetworkLogic.cpp に上記ヘッダで定義したメソッドを実装
    bool NetworkLogic::isRoomExists(void)
    {
        if (mLoadBalancingClient.getRoomList().getIsEmpty()) {
            return false;
        }
    
        return true;
    }
    public:
    void NetworkLogic::sendEvent(nByte code, ExitGames::Common::Hashtable* eventContent)
    {
        mLoadBalancingClient.opRaiseEvent(true, eventContent, 1, code);
    }
  • NetworkLogic.cpp にある customEventAction() を変更
    void NetworkLogic::customEventAction(int playerNr, nByte eventCode, const ExitGames::Common::Object&amp; eventContent)
    {
        ExitGames::Common::Hashtable* event;
    
        switch (eventCode) {
            case 1:
                event = ExitGames::Common::ValueObject&lt;ExitGames::Common::Hashtable*&gt;(eventContent).getDataCopy();
                float x = ExitGames::Common::ValueObject(event-&gt;getValue(1)).getDataCopy();
                float y = ExitGames::Common::ValueObject(event-&gt;getValue(2)).getDataCopy();
                eventQueue.push({static_cast(playerNr), x, y});
                break;
        }
    }
  • NetworkLogic.cpp にある createRoomReturn(), joinRoomReturn(), joinRandomRoomReturn() に追加
        // ルーム内で割り当てられたプレイヤー番号を取得する
        playerNr = localPlayerNr;
  • HelloWorldScene.h に追加
    private:
        virtual void update(float delta);
    
        void addParticle(int playerNr, float x, float y);
    
        NetworkLogic* networkLogic;
  • HelloWorldScene.cpp にて、タッチイベント処理にイベント送信を追加
    bool HelloWorld::onTouchBegan(cocos2d::Touch* touch, cocos2d::Event* event)
    {
        if (networkLogic->playerNr) {
            this->addParticle(networkLogic->playerNr, touch->getLocation().x, touch->getLocation().y);
    
            // イベント(タッチ座標)を送信
            ExitGames::Common::Hashtable* eventContent = new ExitGames::Common::Hashtable();
            eventContent->put<int, float>(1, touch->getLocation().x);
            eventContent->put<int, float>(2, touch->getLocation().y);
            networkLogic->sendEvent(1, eventContent);
        }
    
        return true;
    }
  • HelloWorldScene.cpp に描画更新処理を実装
    void HelloWorld::update(float delta)
    {
        networkLogic->run();
    
        switch (networkLogic->getState()) {
            case STATE_CONNECTED:
            case STATE_LEFT:
                // ルームが存在すればジョイン、なければ作成する
                if (networkLogic->isRoomExists()) {
                    CCLOG("Join");
                    networkLogic->setLastInput(INPUT_JOIN_RANDOM_GAME);
                } else {
                    CCLOG("Create");
                    networkLogic->setLastInput(INPUT_CREATE_GAME);
                }
                break;
            case STATE_DISCONNECTED:
                // 接続が切れたら再度接続
                networkLogic->connect();
                break;
            case STATE_CONNECTING:
            case STATE_JOINING:
            case STATE_JOINED:
            case STATE_LEAVING:
            case STATE_DISCONNECTING:
            default:
                break;
        }
    
        while (!networkLogic->eventQueue.empty()) {
            std::array<float, 3> arr = networkLogic->eventQueue.front();
            networkLogic->eventQueue.pop();
    
            int playerNr = static_cast(arr[0]);
            float x = arr[1];
            float y = arr[2];
            CCLOG("%d, %f, %f", playerNr, x, y);
    
            this->addParticle(playerNr, x, y);
        }
    }
    void HelloWorld::addParticle(int playerNr, float x, float y)
    {
        ParticleSystem* particle;
        switch (playerNr) {
            case 1:
                particle = ParticleFire::create();
                break;
            case 2:
                particle = ParticleSmoke::create();
                break;
            case 3:
                particle = ParticleFlower::create();
                break;
            default:
                particle = ParticleSun::create();
                break;
        }
        particle->setDuration(0.1);
        particle->setSpeed(500);
        particle->setPosition(cocos2d::Point(x,y));
        this->addChild(particle);
    }
  • HelloWorldScene.cpp の init() の最後に追加
        // Photonネットワーククラスのインスタンスを作成
        networkLogic = new NetworkLogic(L"1.0");
    
        scheduleUpdate();
実行結果

このように、他の端末のタップから送信されるイベントで、パーティクルが生成されました!

photon13_complete

 

Androidについても、追加した NetworkLogic.cpp を Android.mk の LOCAL_SRC_FILES に追加すれば
そのままのソースで動きますので、ぜひ試してみてください。

次世代システム研究室では、スマートフォンアプリエンジニアを募集しています。スマホアプリ開発経験者の方、経験は無いけれどぜひやってみたいという意欲のある方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ 募集職種一覧 からご応募をお願いいたします。

皆様からのご応募、お待ちしてます。

Pocket

関連記事