cocos2d-xでsocket.ioを使う Android版

cocos2d-xでsocket.ioを使う iOS版 - おかひろの雑記 ではCocos2d-xでsocket.ioを使うためのiOSの設定をしましたが、今度はAndroidの方を設定します。

Androidではsocket.io-java-client(https://github.com/Gottox/socket.io-java-client)を使いますが、そのまま使うと、切断時にCPU使用率が下がらなくなるバグがあるようなので、少し手を加えます。


今回使用したバージョン

  • Cocos2d-x 3.9
  • socket.io-java-client 最新のもの
  • Java-WebSocket 1.3.0

動作するサンプルはこちら。 https://github.com/okahiro/Cocos2dxSocketIO
(Cocos2d-xのフルプロジェクトなので容量大きいです。socket.ioサーバーを用意する必要があります。)


ライブラリをセットアップ
まずはsocket.io-java-clientとJava-WebSocketをそれぞれダウンロードしてzipを解凍します。

まず、Java-WebSocketの方を解凍したフォルダにターミナルで移動し、antコマンドを実行します。

$ ant

するとdistフォルダにjava_websocket.jarが作成されるので、これをWebSocket.jarという名前に変更し、
socket.io-java-clientを解凍したフォルダのlibsのWebSocket.jarを上書きします。
その後、socket.io-java-clientのフォルダにターミナルで移動し、ant jarコマンドを実行します。

$ ant jar

これによりjarフォルダと中にsocketio.jarが作成されます。
このsocketio.jarをCocos2d-xのプロジェクトに組み込みます。(AndroidStudioを使う場合は、proj.android-studio/app/libsにコピー)


実装
NativeCodeLauncher.hとSocketManagerクラスはcocos2d-xでsocket.ioを使う iOS版 - おかひろの雑記と同じです。

NativeCodeLauncher.cpp

#include "NativeCodeLauncher.h"
#include "NativeCodeLauncherJni.h"

namespace Cocos2dExt
{
	// Socket.IO関連処理
	void NativeCodeLauncher::connectToSocketIO(char const *host,int port)
	{
		connectToSocketIOJNI(host,port);
	}
	void NativeCodeLauncher::emitToSocketIO(char const *event,char const *message)
	{
		emitToSocketIOJNI(event,message);
	}
	void NativeCodeLauncher::disconnectFromSocketIO()
	{
		disconnectFromSocketIOJNI();
	}
}

NativeCodeLauncherJni.h

#include <jni.h>
#include <string>

extern "C"
{
	// Socket.IO関連処理
	extern void connectToSocketIOJNI(char const *host,int port);
	extern void emitToSocketIOJNI(char const *event,char const *message);
	extern void disconnectFromSocketIOJNI();
}

NativeCodeLauncherJni.cpp

#include "NativeCodeLauncherJni.h"
#include <android/log.h>
#include "platform/android/jni/JniHelper.h"
#include "SocketManager.hpp"

#define  LOG_TAG    "NativeCodeLauncher"
#define  LOGD(...)  __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,__VA_ARGS__)
#define CLASS_NAME "org/cocos2dx/cpp/AppActivity"

typedef struct JniMethodInfoEx_
{
    JNIEnv *    env;
    jclass      classID;
    jmethodID   methodID;
} JniMethodInfoEx;

extern "C"
{
	// get env and cache it
	static JNIEnv* getJNIEnv(void)
	{
		JNIEnv *env = 0;

		// get jni environment
		if (cocos2d::JniHelper::getJavaVM()->GetEnv((void**)&env, JNI_VERSION_1_4) != JNI_OK)
		{
			LOGD("Failed to get the environment using GetEnv()");
		}

		if (cocos2d::JniHelper::getJavaVM()->AttachCurrentThread(&env, 0) < 0)
		{
			LOGD("Failed to get the environment using AttachCurrentThread()");
		}

		return env;
	}

	// get class and make it a global reference, release it at endJni().
	static jclass getClassID(JNIEnv *pEnv)
	{
		jclass ret = pEnv->FindClass(CLASS_NAME);
		if (! ret)
		{
			LOGD("Failed to find class of %s", CLASS_NAME);
		}

		return ret;
	}

	static bool getStaticMethodInfo(JniMethodInfoEx &methodinfo, const char *methodName, const char *paramCode)
    {
		jmethodID methodID = 0;
		JNIEnv *pEnv = 0;
		bool bRet = false;

        do 
        {
			pEnv = getJNIEnv();
			if (! pEnv)
			{
				break;
			}

            jclass classID = getClassID(pEnv);

            methodID = pEnv->GetStaticMethodID(classID, methodName, paramCode);
            if (! methodID)
            {
                LOGD("Failed to find static method id of %s", methodName);
                break;
            }

			methodinfo.classID = classID;
			methodinfo.env = pEnv;
			methodinfo.methodID = methodID;

			bRet = true;
        } while (0);

        return bRet;
    }
	
	// Socket.IO
	void connectToSocketIOJNI(char const *host,int port)
	{
		JniMethodInfoEx methodInfo;
		
		if (!getStaticMethodInfo(methodInfo, "connectToSocketIO", "(Ljava/lang/String;I)V"))
		{
			return;
		}
		jstring hostArg = methodInfo.env->NewStringUTF(host);
		
		methodInfo.env->CallStaticVoidMethod(methodInfo.classID, methodInfo.methodID,hostArg,port);
		methodInfo.env->DeleteLocalRef(hostArg);
		methodInfo.env->DeleteLocalRef(methodInfo.classID);
	}
	void emitToSocketIOJNI(char const *event,char const *message)
	{
		JniMethodInfoEx methodInfo;
		
		if (!getStaticMethodInfo(methodInfo, "emitToSocketIO", "(Ljava/lang/String;Ljava/lang/String;)V"))
		{
			return;
		}
		
		jstring eventArg = methodInfo.env->NewStringUTF(event);
		jstring messageArg = methodInfo.env->NewStringUTF(message);
		
		methodInfo.env->CallStaticVoidMethod(methodInfo.classID, methodInfo.methodID, eventArg,messageArg);
		methodInfo.env->DeleteLocalRef(eventArg);
		methodInfo.env->DeleteLocalRef(messageArg);
		methodInfo.env->DeleteLocalRef(methodInfo.classID);
	}
	void disconnectFromSocketIOJNI()
	{
		JniMethodInfoEx methodInfo;
		
		if (!getStaticMethodInfo(methodInfo, "disconnectFromSocketIO", "()V"))
		{
			return;
		}
		
		methodInfo.env->CallStaticVoidMethod(methodInfo.classID, methodInfo.methodID);
		methodInfo.env->DeleteLocalRef(methodInfo.classID);
	}
	// メッセージを受け取った時の処理
	void Java_org_cocos2dx_cpp_AppActivity_nativeReceiveMessage(JNIEnv *env,jobject thiz,
																  jstring event,jstring message)
	{
		const char *eventChar = env->GetStringUTFChars(event,0);
		const char *messageChar = env->GetStringUTFChars(message,0);
		
		SocketManager::getInstance()->receiveMessage(eventChar,messageChar);
		
		env->ReleaseStringUTFChars(event, eventChar);
		env->ReleaseStringUTFChars(message, messageChar);
	}
	// Socket接続された時
	void Java_org_cocos2dx_cpp_AppActivity_nativeOnSocketConnected(JNIEnv *env,jobject thiz)
	{
		SocketManager::getInstance()->onConnected();
	}
	// Socket切断された時
	void Java_org_cocos2dx_cpp_AppActivity_nativeOnSocketDisconnected(JNIEnv *env,jobject thiz)
	{
		SocketManager::getInstance()->onDisconnected();
	}
	// Socketでエラーが発生した時
	void Java_org_cocos2dx_cpp_AppActivity_nativeOnSocketError(JNIEnv *env,jobject thiz,jstring errorMessage)
	{
		const char *errorMessageChar = env->GetStringUTFChars(errorMessage,0);
		
		SocketManager::getInstance()->onError(errorMessageChar);
		
		env->ReleaseStringUTFChars(errorMessage, errorMessageChar);
	}
}

AppActivity.java

package org.cocos2dx.cpp;

import android.util.Log;

import org.cocos2dx.lib.Cocos2dxActivity;
import org.json.JSONObject;

import java.net.MalformedURLException;

import io.socket.*;

public class AppActivity extends Cocos2dxActivity
{
    // C++側のメソッドを呼ぶ宣言
    public static native void nativeReceiveMessage(String event, String message);
    public static native void nativeOnSocketConnected();
    public static native void nativeOnSocketDisconnected();
    public static native void nativeOnSocketError(String errorMessage);

    static private SocketIO socket = null;
    private static final String TAG = "Cocos2dxSocketIOSample";


    // Socket.IO接続
    public static void connectToSocketIO(String host, int port) throws MalformedURLException
    {
        String url = String.format("http://%s:%d", host, port);
        socket = new SocketIO(url);
        socket.connect(iocallback);

        Log.d(TAG, "Socket 接続しようとします");
    }

    // Socketにイベント送信
    public static void emitToSocketIO(String event, String message)
    {
        if (socket != null && socket.isConnected())
        {
            // イベント送信
            socket.emit(event, message);
            Log.d(TAG, "emit. " + event + " , " + message);
        }
        else if(socket != null && !socket.isConnected())
        {
            // Socket接続されていないのに送信しようとした場合
            AppActivity.nativeOnSocketError("Socket is not connected.");
        }
    }

    // Socket.IO切断
    public static void disconnectFromSocketIO()
    {
        if(socket != null && socket.isConnected())
        {
            socket.disconnect();

            Log.d(TAG, "Socket 切断します");
        }
    }

    // Socket.IOコールバック関数集合
    private static IOCallback iocallback = new IOCallback()
    {
        @Override
        public void onConnect()
        {
            Log.d(TAG, "Socket 接続されました");
            AppActivity.nativeOnSocketConnected();
        }

        @Override
        public void onDisconnect()
        {
            Log.d(TAG, "Socket 切断されました");
            AppActivity.nativeOnSocketDisconnected();
        }

        @Override
        public void onMessage(JSONObject json, IOAcknowledge ack)
        {
            Log.d(TAG, "Socket onMessage");
        }

        @Override
        public void onMessage(String data, IOAcknowledge ack)
        {
            Log.d(TAG, "Socket onMessage");
        }

        @Override
        public void on(String event, IOAcknowledge ack, Object... args)
        {
            Log.d(TAG, "received . " + args[0]);
            AppActivity.nativeReceiveMessage(event, String.valueOf(args[0]));
        }

        @Override
        public void onError(SocketIOException socketIOException)
        {
            Log.d(TAG, "Socket エラー発生しました");
            socketIOException.printStackTrace();
            AppActivity.nativeOnSocketError(socketIOException.getLocalizedMessage());
        }
    };
}

cocos2d-xでsocket.ioを使う iOS版

Cocos2d-xでsocket.ioを使う方法について調べました。
Cocos2d-xには標準でSocketIOクラスが用意されていますが、諸々の理由により使わずに、
iOS/Androidそれぞれ個別に設定を行います。

まずはiOSから。

iOSはAZSocketIOを使用します。AZSocketIOはcocoapodsを使用してインストールするのが一般的のようですが、
Cocos2d-xでcocoapodsを使えるようにするのも面倒そうなので、手動で設定します。

今回使用したバージョン

  • Cocos2d-x 3.9
  • AZSocketIO 0.0.6
  • SocketRocket 0.4.2
  • AFNetworking 2.6.3 (3.1.0だとビルドできませんでした)


ライブラリをセットアップ
まずはAZSocketIOと、それに必要なAFNetworking、SocketRocketをCocos2d-xプロジェクトに組み込みます。

下記サイトからそれぞれダウンロードしてzipを解凍します。

解凍したら、それぞれの中にある下記フォルダをCocos2d-xプロジェクトのproj.ios_macなどにコピーして、Xcode上でプロジェクトに追加します。

  • AFNetworking
  • SocketRocket
  • AZSocketIO

次に、Xcode上の「Compile Sources」で、AZ*、AF*、SR*の全てに-fobjc-arcを設定します。

「Link Binary With Libraries」に下記を追加します。

  • SystemConfiguration
  • CFNetwork
  • MobileCoreServices
  • libicucore.tbd

ここまでで、ビルドができるようになるかと思います。


実装
socket.ioサーバーへの接続や切断などはObjective-Cのコードで記述するので、NativeCodeLauncherというクラスに実装します。
また、接続完了した、切断した、エラーが発生したなどのイベントを受け取ることができますが、これらを受け取った時に
Cocos2d-x側でなにかしらのアクションを行えるよう、SocketManagerというシングルトンクラスを作っておきます。
Cocos2d-xのプログラムからSocket.io関連の何かをする場合は全てSocketManagerを通すという設計にします。

NativeCodeLauncher.h

#include <stddef.h>
#include <string>

namespace Cocos2dExt {
    class NativeCodeLauncher
    {
    public:
		// Socket.IO関連
		static void connectToSocketIO(const char* host,int port);
		static void emitToSocketIO(const char* event,const char* message);
		static void disconnectFromSocketIO();
    };
} // end of namespace Cocos2dExt

NativeCodeLauncher.mm

#include "NativeCodeLauncher.h"
#include "NativeCodeLauncher_objc.h"

// Socket.IO関連処理
static void static_connectToSocketIO(const char* host,int port)
{
	[[NativeCodeLauncher sharedManager] connectToSocketIO: [NSString stringWithUTF8String:host] port: port];
}
static void static_emitToSocketIO(const char* event,const char* message)
{
	[[NativeCodeLauncher sharedManager] emitToSocketIO:[NSString stringWithUTF8String:event]
											   message:[NSString stringWithUTF8String:message]];
}
static void static_disconnectFromSocketIO()
{
	[[NativeCodeLauncher sharedManager] disconnectFromSocketIO];
}

namespace Cocos2dExt
{
	// ソケット接続
	void NativeCodeLauncher::connectToSocketIO(const char* host,int port)
	{
		static_connectToSocketIO(host,port);
	}
	// イベントとメッセージをソケットに送信
	void NativeCodeLauncher::emitToSocketIO(const char* event,const char* message)
	{
		static_emitToSocketIO(event,message);
	}
	// ソケット切断
	void NativeCodeLauncher::disconnectFromSocketIO()
	{
		static_disconnectFromSocketIO();
	}
}

NativeCodeLauncher_objc.h

@interface NativeCodeLauncher : NSObject{
	
}

// シングルトンオブジェクトを返す
+ (NativeCodeLauncher *)sharedManager;

// Socket.IO
-(void)connectToSocketIO:(NSString *)host port:(int) port;
-(void)emitToSocketIO:(NSString *)event message:(NSString *)message;
-(void)disconnectFromSocketIO;

@end

NativeCodeLauncher_objc.mm

#import "NativeCodeLauncher_objc.h"
//#import "EAGLView.h"
#import "AppController.h"
#import "RootViewController.h"
#import "AZSocketIO.h"
#import "SocketManager.hpp"

@implementation NativeCodeLauncher
{
	AZSocketIO *socketIO;
}

// シングルトンオブジェクト
static NativeCodeLauncher *sharedData_ = nil;

// シングルトンオブジェクトを返す
+ (NativeCodeLauncher *)sharedManager
{
	@synchronized(self)
	{
		if(!sharedData_)
		{
			sharedData_ = [NativeCodeLauncher new];
		}
	}
	
	return sharedData_;
}
- (id)init
{
	self = [super init];
	if(self)
	{
		
	}
	
	return self;
}

// ソケット接続
-(void)connectToSocketIO:(NSString *)host port:(int) port
{
	// ホストとポート番号を指定してAZSocketIOインスタンス生成
	socketIO = [[AZSocketIO alloc] initWithHost:host andPort:[NSString stringWithFormat:@"%d",port] secure:NO];
	socketIO.reconnectionDelay = 1.0;
	socketIO.reconnectionLimit = 4;
	socketIO.maxReconnectionAttempts = 1;
	
	// 接続開始
	[socketIO connectWithSuccess:^{
		NSLog(@"Socket接続成功.");
		// 接続成功
		SocketManager::getInstance()->onConnected();
	} andFailure:^(NSError *error) {
		NSLog(@"Socket接続失敗. error: %@", error);
		// 接続失敗
		NSString *errorMessage = [NSString stringWithFormat:@"%@", error];
		SocketManager::getInstance()->onError([errorMessage UTF8String]);
	}];
	
	// イベントを受けとった時
	[socketIO setEventReceivedBlock:^(NSString *eventName, id data) {
		// ただのキャストではうまくいかない
		NSString *message = [NSString stringWithFormat:@"%@",data[0]];
		NSLog(@"Event : %@, message : %@",eventName,message);
		// イベントとメッセージを渡して仕分け
		SocketManager::getInstance()->receiveMessage([eventName UTF8String], [message UTF8String]);
	}];
	
	// エラーを受信したときに実行されるBlocks
	[socketIO setErrorBlock:^(NSError *error) {
		NSLog(@"error: %@", error);
		// エラー
		NSString *errorMessage = [NSString stringWithFormat:@"%@", error];
		SocketManager::getInstance()->onError([errorMessage UTF8String]);
	}];
	
	// 切断されたときに実行されるBlocks
	[socketIO setDisconnectedBlock:^{
		NSLog(@"Socket切断完了.");
		// 切断
		SocketManager::getInstance()->onDisconnected();
	}];
}
// イベントとメッセージをソケットに送信
-(void)emitToSocketIO:(NSString *)event message:(NSString *)message
{
	[socketIO emit:event args:message error:NULL];
}
// ソケット切断
-(void)disconnectFromSocketIO
{
	[socketIO disconnect];
}

@end

SocketManager.hpp

#include "cocos2d.h"

USING_NS_CC;

class SocketManager
{
private:
	static SocketManager* mManager;
public:
	SocketManager();
	static SocketManager* getInstance();
	
	// ソケット接続/切断
	void connect();
	void disconnect();
	// 接続された
	void onConnected();
	// 切断された
	void onDisconnected();
	// エラーが発生した
	void onError(std::string error);
	// メッセージを送信
	void emitMessage(std::string event,std::string message);
	// メッセージを受け取った時の処理
	void receiveMessage(std::string event,std::string message);
};

SocketManager.cpp

#include "SocketManager.hpp"
#include "NativeCodeLauncher.h"

SocketManager* SocketManager::mManager = NULL;

#pragma mark - 初期化

SocketManager::SocketManager()
{
	
}

SocketManager* SocketManager::getInstance()
{
	if(mManager == NULL)
	{
		mManager = new SocketManager();
	}
	
	return mManager;
}

// Socket接続
void SocketManager::connect()
{
	// 接続先のホスト、ポートは変更
	Cocos2dExt::NativeCodeLauncher::connectToSocketIO("192.168.0.1", 3150);
}
// Socket切断
void SocketManager::disconnect()
{
	Cocos2dExt::NativeCodeLauncher::disconnectFromSocketIO();
}

// Socket接続された
void SocketManager::onConnected()
{
	// Cocos2d-xのスレッドに実行させる。これがないと、Androidの描画関連がうまくいかないことがある。
	Director::getInstance()->getScheduler()->performFunctionInCocosThread([=](){
		// イベントを通知
		Director::getInstance()->getEventDispatcher()->dispatchCustomEvent("SocketConnected");
	});
}
// Socket切断された
void SocketManager::onDisconnected()
{
	// Cocos2d-xのスレッドに実行させる。これがないと、Androidの描画関連がうまくいかないことがある。
	Director::getInstance()->getScheduler()->performFunctionInCocosThread([=](){
		// イベントを通知
		Director::getInstance()->getEventDispatcher()->dispatchCustomEvent("SocketDisconnected");
	});
}
// Socketでエラーが発生した
void SocketManager::onError(std::string error)
{
	// Cocos2d-xのスレッドに実行させる。これがないと、Androidの描画関連がうまくいかないことがある。
	Director::getInstance()->getScheduler()->performFunctionInCocosThread([=](){
		// イベントを通知
		Director::getInstance()->getEventDispatcher()->dispatchCustomEvent("SocketError");
	});
}
// イベントを送信
void SocketManager::emitMessage(std::string event, std::string message)
{
	Cocos2dExt::NativeCodeLauncher::emitToSocketIO(event.c_str(),message.c_str());
}
// イベントを受信した
void SocketManager::receiveMessage(std::string event, std::string message)
{
	// Cocos2d-xのスレッドに実行させる。これがないと、Androidの描画関連がうまくいかないことがある。
	Director::getInstance()->getScheduler()->performFunctionInCocosThread([=](){
		// イベントを通知
		EventCustom customEvent("SocketEventReceived");
		auto messageValue = Value(message);
		customEvent.setUserData(&messageValue);
		// 受け取った内容をEventCustomに入れて通知
		Director::getInstance()->getEventDispatcher()->dispatchEvent(&customEvent);
	});
}

cocos2d-x + Box2d 接触している物理体を調べる

Box2dでの衝突検出はb2ContactListenerがありますが、これは衝突したときに呼ばれるもので、
そのときに衝突した物理体の情報のみ取得することができます。

そうではなく、ある瞬間で、物理体が接触している他の物理体の種類や個数を知りたいことがあります。

特定の物理体が接触している情報を表示するサンプルを作ってみました。

今回使用したバージョン

  • Cocos2d-x 3.9


動作するサンプルはこちら。 https://github.com/okahiro/Cocos2dxBox2d
(Cocos2d-xのフルプロジェクトなので容量大きいです)


画面上にボタンを作成し、そのボタンを押したら、それぞれの接触情報を表示するようにしてみます。

bool Box2dAndPEScene::init()
{
       ...

	// 接触情報取得ボタン
	ui::Button *infoButton = ui::Button::create();
	infoButton->setTitleText("接触情報取得");
	infoButton->setTitleFontSize(26);
	infoButton->setPosition(Vec2(winSize.width * 0.85f,winSize.height * 0.75f));
	infoButton->addTouchEventListener([=](Ref* pSender,ui::Widget::TouchEventType type)
	  {
		  if(type == ui::Widget::TouchEventType::ENDED)
		  {
			  std::string logString = "■接触情報■\n";
			  
			  for (b2Body *b = _world->GetBodyList(); b; b = b->GetNext())
			  {
				  if (b->GetUserData() != nullptr && b->GetLinearVelocity().Length() < 2.0f)
				  {
					  auto node = (Node *)b->GetUserData();
					  
					  // タグ。0は地面、1移行はブロック
					  int tag = node->getTag();
					  logString.append(StringUtils::format("物理体No%d\n",tag));
						  
					  // 物理体から、現時点のコンタクトのリストを取得する。同じ物理体に二箇所以上で接触している場合、その箇所分
					  for (b2ContactEdge* ce = b->GetContactList(); ce; ce = ce->next)
					  {
						  b2Contact* c = ce->contact;
						  // 接触していなければ終了
						  if(!c->IsTouching())
						  {
							  continue;
						  }
						  
						  // b2Contactから、2つのオブエジェクトを取得する。
						  b2Body *bA = c->GetFixtureA()->GetBody();
						  b2Body *bB = c->GetFixtureB()->GetBody();
						  Node *nodeA = (Node*)bA->GetUserData();
						  Node *nodeB = (Node*)bB->GetUserData();
						  
						  int contactNodeTag = -1;
						  
						  if(nodeA->getTag() == tag)
						  {
							  contactNodeTag = nodeB->getTag();
						  }
						  else if(nodeB->getTag() == tag)
						  {
							  contactNodeTag = nodeA ->getTag();
						  }
						  else
						  {
							  continue;
						  }
						  
						  logString.append(StringUtils::format("┗接触物理体No%d\n",contactNodeTag));
					  }
				  }
			  }
			  
			  // ログに表示
			  CCLOG("%s",logString.c_str());
			  
			  // ラベルがあれば削除
			  this->removeChildByTag(999);
			  // ラベルを作って表示
			  Label *infoLabel = Label::createWithSystemFont(logString, "", 20);
			  infoLabel->setTag(999);
			  //infoLabel->setAlignment(TextHAlignment::LEFT, TextVAlignment::TOP);
			  infoLabel->setAnchorPoint(Vec2(0.0f,1.0f));
			  infoLabel->setPosition(Vec2(5,winSize.height * 0.85f));
			  this->addChild(infoLabel);
		  }
	  });
	this->addChild(infoButton);

        ...

b2BodyのGetContactListメソッドを使って接触情報を取得します。
物理体のNode全てに別の番号でTagをつけているので、相手のTagを調べてどの物理体と接触しているのかを
調べます。
地面のTagは0にしています。

ちなみに物理体AとBが二箇所以上で接触している場合、GetContactListメソッドはその箇所分の情報を返すようです。

cocos2d-x + Box2d 物理体のデバッグ表示

物理体のデバッグ表示です。

今回使用したバージョン

  • Cocos2d-x 3.9

こちらを参考にさせていただきました。
cocos2d-x 3.2 + Box2D iOSでDebugDrawの実行まで - Qiita

動作するサンプルはこちら。 https://github.com/okahiro/Cocos2dxBox2d
(Cocos2d-xのフルプロジェクトなので容量大きいです)

実装
Box2dAndPEScene.hpp

class Box2dAndPEScene : public cocos2d::Layer
{
private:
	b2World *_world;	// 物理ワールド
	GLESDebugDraw *_debugDraw;	// 物理体デバッグ表示
	
	int _blockNo = 0;	// ブロックにつける番号
private:
	Box2dAndPEScene();
	~Box2dAndPEScene();
	
public:
	virtual bool init() override;
	static cocos2d::Scene* createScene();
	CREATE_FUNC(Box2dAndPEScene);
	
	void update(float delta) override;
	
	virtual void draw(cocos2d::Renderer* renderer, const cocos2d::Mat4 &transform, uint32_t flags) override;
	
	// ブロックを作成
	void createBlock(int x,int y);
	// 地面を作成
	void createGround();
};

GLESDebugDraw型のプロパティとdrawメソッドを定義します。


Box2dAndPEScene.cpp

Box2dAndPEScene::Box2dAndPEScene() : _world(nullptr),_debugDraw(nullptr)
{
	
}
Box2dAndPEScene::~Box2dAndPEScene()
{
	// b2Worldを解放
	if(_world)
	{
		delete _world;
		_world = nullptr;
		
		CCLOG("b2World has been safely deleted.");
	}
	// GLESDebugDrawを解放
	if(_debugDraw)
	{
		delete _debugDraw;
		_debugDraw = nullptr;
		
		CCLOG("DebugDraw has been safely deleted.");
	}
}

// 初期化
bool Box2dAndPEScene::init()
{
	if ( !Layer::init() )
	{
		return false;
	}
	
	Size winSize = Director::getInstance()->getWinSize();
	
	// 物理設定
	b2Vec2 gravity;
	gravity.Set(0.0f, -25.0f);	// 重力の値は動きを見ながら調整
	
	// World作成
	_world = new b2World(gravity);
	_world->SetAllowSleeping(true);
	_world->SetContinuousPhysics(true);
	
	// DebugDrawを設定
	_debugDraw = new GLESDebugDraw(PTM_RATIO);
	_world->SetDebugDraw(_debugDraw);
	
	uint32 flags = 0;
	flags += b2Draw::e_shapeBit;
	//        flags += b2Draw::e_jointBit;
	//        flags += b2Draw::e_aabbBit;
	flags += b2Draw::e_pairBit;
	//        flags += b2Draw::e_centerOfMassBit;
	this->_debugDraw->SetFlags(flags);

        ...


// デバッグ情報表示
void Box2dAndPEScene::draw(cocos2d::Renderer* renderer, const cocos2d::Mat4& transform, uint32_t flags) {
	Layer::draw(renderer, transform, flags);
	Director* director = Director::getInstance();
	
	GL::enableVertexAttribs( cocos2d::GL::VERTEX_ATTRIB_FLAG_POSITION );
	director->pushMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);
	this->_world->DrawDebugData();
	director->popMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);
}

コンストラクタでnullptr初期化、デストラクタで解放します。
initメソッドでオブジェクト生成し、worldに設定します。

描画はdrawメソッドで行います。

注意点として、物理体にSpriteを設定している場合、デバッグ表示がSpriteの裏に隠れてしまいます。
Spriteを非表示にするか、半透明にするなどする必要があります。

cocos2d-x + Box2d PhysicsEditorで作った物理体を拡大/縮小してみる

Box2dの物理体を拡大/縮小したいことがありますが、
単純にSpriteをsetScaleしただけでは、見た目は変わっても、物理体は変わりません。

cocos2d-x + Box2d PhysicsEditorで作ったデータを読み込む - おかひろの雑記で作ったサンプルに、拡大/縮小処理を入れてみました。

今回使用したバージョン

  • Cocos2d-x 3.9
  • PhysicsEditor 1.5.2

動作するサンプルはこちら。 https://github.com/okahiro/Cocos2dxBox2d
(Cocos2d-xのフルプロジェクトなので容量大きいです)

createBlockメソッドで物理体を作成したらすぐにサイズも指定します。

void Box2dAndPEScene::createBlock(int x,int y)
{
	_blockNo++;
	
	// ブロックの名前(PhysicsEditer上でつけた名前)
	std::string blockName = "";
	// Noによってブロックを変える
	switch(_blockNo % 4)
	{
		case 0:
			blockName = "circle";
			break;
		case 1:
			blockName = "cross";
			break;
		case 2:
			blockName = "square";
			break;
		case 3:
			blockName = "triangle";
			break;
		default:
			blockName = "circle";
			break;
	}
	
	// Sprite
	Sprite *block = Sprite::create(StringUtils::format("res/%s.png",blockName.c_str()));
	block->setTag(_blockNo);		// ブロックに番号をつける
	this->addChild(block);
	
	b2BodyDef bodyDef;
	bodyDef.type = b2_dynamicBody;
	bodyDef.position.Set(x / PTM_RATIO,y / PTM_RATIO);
	
	b2Body *body = this->_world->CreateBody(&bodyDef);
	gbox2d::GB2ShapeCache::getInstance()->addFixturesToBody(body, blockName);	// bodyに、plist上の指定の物理体情報でFixture作成
	body->SetUserData(block);
	
	// AnchorPointを取得して設定
	block->setAnchorPoint(gbox2d::GB2ShapeCache::getInstance()->anchorPointForShape(blockName));
	
	// ブロックのサイズ変更
	// 大きさ 0.75倍、等倍、1.25倍
	float scale = 0.75f + ((int)(CCRANDOM_0_1() * 100) % 3) * 0.25f;
	
	if(scale != 1.0f)
	{
		// 物理構造のスケーリング
		for (b2Fixture* f = body->GetFixtureList(); f; f = f->GetNext())
		{
			b2Shape *shape = f->GetShape();
			if(shape->GetType() == b2Shape::e_circle)
			{
				// 円の場合は半径をスケール
				body->GetFixtureList()->GetShape()->m_radius *= scale;
			}
			else
			{
				// 円以外の場合
				b2PolygonShape *poly = (b2PolygonShape*)shape;
				int vCount = poly->GetVertexCount();	// 頂点の数
				b2Vec2 vertices[b2_maxPolygonVertices];
				// 全ての頂点にscaleをかける
				for(int i = 0; i < vCount; i++)
				{
					b2Vec2 v = poly->GetVertex(i);
					vertices[i].x = v.x * scale;
					vertices[i].y = v.y * scale;
				}
				// 新しい頂点を設定
				poly->Set(vertices, vCount);
			}
		}
		
		// スプライトもサイズ変更
		block->setScale(scale);
	}
	
	CCLOG("■ブロック%dを作成しました。サイズ:%f",_blockNo,scale);
}

bodyからb2Fixtureを取得し、さらにb2Shapeを取得します。
形が円の場合は単純に半径に倍率をかけ、そうでないときは全頂点を取得して倍率をかけます。

cocos2d-x + Box2d PhysicsEditorで作ったデータを読み込む

cocos2d-xでBox2dを使う - おかひろの雑記 で、Cocos2d-xでBox2dを使う設定をしましたが、
実際にゲームで使いたい物理オブジェクトの形は複雑なことが多いので、
物理体の座標を個別に設定するのは相当難しいと思います。

なので、PhysicsEditor(https://www.codeandweb.com/physicseditor)を使って物理体の定義を行い、そのデータをCocos2d-xで読み込むようにしてみます。

Cocos2d-x側でPhysicsEditorのデータを読み込むには、GitHub - tks2shimizu/GB2ShapeCache-x-for-cocos2d-x-3.x: GB2ShapeCache-x for cocos2d-x 3.xを使うと良いと思います。

今回使用したバージョン

  • Cocos2d-x 3.9
  • PhysicsEditor 1.5.2

動作するサンプルはこちら。 https://github.com/okahiro/Cocos2dxBox2d

準備
GB2ShapeCache-xをダウンロードし、プロジェクトに追加します。

そのままではコンパイルが通らないので、includeの部分を修正します。

修正箇所

#include "cocoa/CCNS.h"#include "CCNS.h"

また、addShapesWithFileメソッドでメモリリークしてるっぽいので、最後にdelete dict;を追加します。
(間違っていたらすみません)

     ....

            } else if (fixtureType == "CIRCLE") {
                auto fix = new FixtureDef();
                fix->fixture = basicData; // copy basic data
                fix->callbackData = callbackData;
                
                auto circleData = (Dictionary *)fixtureData->objectForKey("circle");
                
                auto circleShape = new b2CircleShape();
                
                circleShape->m_radius = static_cast<String *>(circleData->objectForKey("radius"))->floatValue() / ptmRatio;
                auto p = PointFromString(static_cast<String *>(circleData->objectForKey("position"))->getCString());
                circleShape->m_p = b2Vec2(p.x / ptmRatio, p.y / ptmRatio);
                fix->fixture.shape = circleShape;
                
                // create a list
                *nextFixtureDef = fix;
                nextFixtureDef = &(fix->next);
                
            } else {
                CCAssert(0, "Unknown fixtureType");
            }
            
            // add the body element to the hash
            shapeObjects[bodyName->getCString()] = bodyDef;
            
        }
        
    }
	
    delete dict;  // これを追加
}

plistファイルの作成
PhysicsEditorを起動し、物理体を定義します。

サンプルで4つの物理体を作成しました。
"circle"、"cross"、"square"、"triangle"などの名前は後で使うので、きちんとした名前をつけておく必要があります。

Publishでplistを作成します。

plistファイルの読み込み
initメソッドでplistの読み込みを行います。

// 初期化
bool Box2dAndPEScene::init()
{
	if ( !Layer::init() )
	{
		return false;
	}
	
	Size winSize = Director::getInstance()->getWinSize();
	
	// 物理設定
	b2Vec2 gravity;
	gravity.Set(0.0f, -25.0f);	// 重力の値は動きを見ながら調整
	
	// World作成
	_world = new b2World(gravity);
	_world->SetAllowSleeping(true);
	_world->SetContinuousPhysics(true);
	
	// PhysicsEditorから出力したplistを読み込み
	gbox2d::GB2ShapeCache::getInstance()->addShapesWithFile("res/shapes.plist");
	
	// 画面をタップしたら
	auto listenerForSprite = EventListenerTouchOneByOne::create();
	listenerForSprite->setSwallowTouches(true);
	listenerForSprite->onTouchBegan = [=](Touch* touch, Event* event)
	{
		Vec2 pos = touch->getLocation();
		// ブロックを作成
		this->createBlock(pos.x, pos.y);
		
		return true;
	};
	Director::getInstance()->getEventDispatcher()->addEventListenerWithSceneGraphPriority(listenerForSprite, this);
	
	// 地面を作成
	this->createGround();
	
	// updateメソッドを開始
	this->scheduleUpdate();
	
	return true;
}

物理体作成
大分シンプルになりました。
PhysicsEditor上でつけた名前を指定して物理体を作成します。

// ブロックを作成する
void Box2dAndPEScene::createBlock(int x,int y)
{
	_blockNo++;
	
	// ブロックの名前(PhysicsEditer上でつけた名前)
	std::string blockName = "";
	// Noによってブロックを変える
	switch(_blockNo % 4)
	{
		case 0:
			blockName = "circle";
			break;
		case 1:
			blockName = "cross";
			break;
		case 2:
			blockName = "square";
			break;
		case 3:
			blockName = "triangle";
			break;
		default:
			blockName = "circle";
			break;
	}
	
	// Sprite
	Sprite *block = Sprite::create(StringUtils::format("res/%s.png",blockName.c_str()));
	block->setTag(_blockNo);		// ブロックに番号をつける
	this->addChild(block);
	
	b2BodyDef bodyDef;
	bodyDef.type = b2_dynamicBody;
	bodyDef.position.Set(x / PTM_RATIO,y / PTM_RATIO);
	
	b2Body *body = this->_world->CreateBody(&bodyDef);
	gbox2d::GB2ShapeCache::getInstance()->addFixturesToBody(body, blockName);	// bodyに、plist上の指定の物理体情報でFixture作成
	body->SetUserData(block);
	
	// AnchorPointを取得して設定
	block->setAnchorPoint(gbox2d::GB2ShapeCache::getInstance()->anchorPointForShape(blockName));
	
	
	CCLOG("■ブロック%dを作成しました。",_blockNo);
}

これで、画面をタップすると十字、正方形、三角形、円のオブジェクトが順番にでてくるようになりました。

cocos2d-xでBox2dを使う

ゲームに物理エンジンを使いたいことはよくあります。

Cocos2d-xは標準でChipmunkをラップした仕組みがあって便利そうに見えますが、iPhone5s以上(?)の実機で動かすと
挙動がおかしく、あまり使い物にならなそうです。
これはCocos2d-xに添付されているcocos2d_testsプロジェクトでも発生します。
例えば「40:Node:Physics」の「3:PhysicsComponentTest」でおじさんをたくさん置くと、何もしていないのに勝手にピクピク動き始めます。
動画: https://youtu.be/M7TJlyksQzU

仕方がないのでBox2dを使うことにします。

今回使用したバージョン

  • Cocos2d-x 3.9

Box2dはCocos2d-xに含まれているので、サイトからダウンロードして組み込んだりする必要はありません。

タップした座標に丸い物理オブジェクトを生成するサンプルを作ってみます。
画面の下には地面を作ります。
動作するサンプルはこちら。 https://github.com/okahiro/Cocos2dxBox2d

Box2dSceneというクラスを作成し、このScene上では物理エンジンが働くようにします。

Box2dScene.hpp

#include "cocos2d.h"
#include "ui/CocosGUI.h"
#include "Box2D/Box2D.h"

static const float PTM_RATIO = 32.0f;
static const float TIME_STEP = 1.0f / 60.0f;
static const int VELOCITY_ITERATIONS = 8;
static const int POSITION_ITERATIONS = 3;

// シーン
class Box2dScene : public cocos2d::Layer
{
private:
	b2World *_world;	// 物理ワールド
	
	int _blockNo = 0;	// ブロックにつける番号
private:
	Box2dScene();
	~Box2dScene();
	
public:
	virtual bool init() override;
	static cocos2d::Scene* createScene();
	CREATE_FUNC(Box2dScene);
	
	void update(float delta) override;
	
	// ブロックを作成
	void createBlock(int x,int y);
	// 地面を作成
	void createGround();
};

Box2dScene.cpp

コンストラクタ/デストラク

USING_NS_CC;

#pragma mark - Box2dScene
Box2dScene::Box2dScene() : _world(nullptr)
{
	
}
Box2dScene::~Box2dScene()
{
	// b2Worldを解放
	if(_world)
	{
		delete _world;
		_world = nullptr;
		
		CCLOG("b2World has been safely deleted.");
	}
}

デストラクタでb2Worldを解放します。

シーン初期化など

#pragma mark - シーン関連

Scene* Box2dScene::createScene()
{
	auto scene = Scene::create();
	
	auto layer = Box2dScene::create();
	
	scene->addChild(layer);
	
	return scene;
}

// 初期化
bool Box2dScene::init()
{
	if ( !Layer::init() )
	{
		return false;
	}
	
	Size winSize = Director::getInstance()->getWinSize();
	
	// 物理設定
	b2Vec2 gravity;
	gravity.Set(0.0f, -25.0f);	// 重力の値は動きを見ながら調整
	
	// World作成
	_world = new b2World(gravity);
	_world->SetAllowSleeping(true);
	_world->SetContinuousPhysics(true);
	
	// 画面をタップしたら
	auto listenerForSprite = EventListenerTouchOneByOne::create();
	listenerForSprite->setSwallowTouches(true);
	listenerForSprite->onTouchBegan = [=](Touch* touch, Event* event)
	{
		Vec2 pos = touch->getLocation();
		// ブロックを作成
		this->createBlock(pos.x, pos.y);
		
		return true;
	};
	Director::getInstance()->getEventDispatcher()->addEventListenerWithSceneGraphPriority(listenerForSprite, this);
	
	// 地面を作成
	this->createGround();
	
	// updateメソッドを開始
	this->scheduleUpdate();
	
	return true;
}

// updateメソッドでBox2dWorldにある物理オブジェクトとNodeの位置と角度を合わせる
void Box2dScene::update(float delta)
{
	_world->Step(TIME_STEP, VELOCITY_ITERATIONS, POSITION_ITERATIONS);
	
	for (b2Body *b = _world->GetBodyList(); b; b = b->GetNext())
	{
		if (b->GetUserData() != nullptr)
		{
			auto myActor = (Node *)b->GetUserData();
			myActor->setPosition(b->GetPosition().x * PTM_RATIO,b->GetPosition().y * PTM_RATIO);
			myActor->setRotation(-1 * CC_RADIANS_TO_DEGREES(b->GetAngle()));
		}
	}
	
	// 画面外に落ちたブロックを削除
	b2Body* node = this->_world->GetBodyList();
	
	while (node)
	{
		b2Body* b = node;
		node = node->GetNext();
		
		Node *n = (Node*)b->GetUserData();
		
		// 物理オブジェクトの高さが一定以下になったら削除
		if (n->getPositionY() < -100)
		{
			this->_world->DestroyBody(b);
			CCLOG("■ブロック%dが削除されました。",n->getTag());
			this->removeChild(n);
		}
	}
}

initメソッド内で物理ワールドを生成します。
updateメソッドの中で、物理オブジェクトの位置と回転をスプライトに反映させます。
また、画面外に落ちていった物理オブジェクトは消去する処理も入れています。

物理オブジェクト(ブロック/地面)作成

// ブロックを作成する
void Box2dScene::createBlock(int x,int y)
{
	_blockNo++;
	
	// Sprite
	Sprite *block = Sprite::create("res/circle.png");
	block->setTag(_blockNo);		// ブロックに番号をつけてみる
	this->addChild(block);
	
	// 物理オブジェクトBody
	b2Body *blockBody;
	// 物理オブジェクトBody定義
	b2BodyDef blockBodyRef;
	blockBodyRef.type = b2_dynamicBody;
	blockBodyRef.position.Set(x / PTM_RATIO, y / PTM_RATIO);
	blockBodyRef.userData = block;
	blockBody = _world->CreateBody(&blockBodyRef);
	
	// 形状は円
	b2CircleShape circleShape;
	circleShape.m_radius = block->getContentSize().width * 0.5f / PTM_RATIO;
	
	b2FixtureDef circleShapeRef;
	circleShapeRef.shape = &circleShape;
	circleShapeRef.density = 10.0f;		// 密度
	circleShapeRef.friction = 1.0f;		// 摩擦
	circleShapeRef.restitution = 0.0f;	// 反発
	
	// Fixture(これがないと、ブロック同士が衝突判定されない)
	blockBody->CreateFixture(&circleShapeRef);
	
	
	CCLOG("■ブロック%dを作成しました。",_blockNo);
}
// 地面を作成する
void Box2dScene::createGround()
{
	Size winSize = Director::getInstance()->getWinSize();
	
	// Node
	Node *ground = Node::create();
	ground->setPosition(Vec2::ZERO);
	this->addChild(ground);
	
	// 物理オブジェクトBody
	b2Body *groundBody;
	// 物理オブジェクトBody定義
	b2BodyDef groundBodyDef;
	groundBodyDef.position.Set(ground->getPositionX() / PTM_RATIO,ground->getPositionY() / PTM_RATIO);
	groundBodyDef.userData = ground;
	groundBody = _world->CreateBody(&groundBodyDef);
	
	// 地面 両端は落ちるようになっている
	b2EdgeShape groundShape;
	groundShape.Set(b2Vec2(winSize.width * 0.2f / PTM_RATIO,0),b2Vec2(winSize.width * 0.8f / PTM_RATIO,0));
	groundBody->CreateFixture(&groundShape, 0);
}

これで、タップしたところに丸いオブジェクトが作成され、落ちていくようになります。