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);
}

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