cocos2d-x
Box2D
LiquidFun

LiquidFunを使う -物理エンジンとゲームエンジンを合わせる仕組み-

More than 3 years have passed since last update.

アドベントカレンダー7日目、LiquidFunを扱う記事を書きます!

タップしたらそこから水が現れる、サイババ :man_with_turban: になった気分になれるアプリを作ってみましょう。

今回作成したプロジェクトはこちらにあります!

参考になれば幸いです!

liquidfun-logo-small.png


LiquidFunとは

Box2Dを拡張したもので、Google製の流体演算ライブラリです。

流体は小さな粒子の集合をマクロに扱うことで再現されていますが、このライブラリのポイントは流体の粒子の回転を考慮せずに演算を行うため、パフォーマンスが出やすく、速度もわりと快適なところです。


Box2dとは

Box2Dとは、物理演算ライブラリのことです。Cocos2d-xでは、主要な物理演算ライブラリとしては、Chipmunkが採用されていますが、少し前まではChipmunkとBox2dどちらも対応していましたが今ではChipmunkがメインとなっています。

少し影の存在になってしまったBox2Dでしたが、このLiquidfunのおかげで少し盛り返してきてたりします。


1.Box2Dの差し替え

まず、Cocos2d-xのデフォルトのフォルダに入っているBox2Dを削除して、Githubのほうからliquidfunのフォルダーをダウンロードしてきましょう。ダウンロードの内部のうちから

liquidfun-masterのフォルダ-liquidfun-Box2D-Box2Dフォルダをコピーして、Classesと同ディレクトリに入れてみましょう。

Box2D/->にコピーしたフォルダをコピー

Classes/

proj.ios_mac

Xcode上でHeader Search Pathに

$(SRCROOT)/../Box2D

を追加するのを忘れずに。詳細はgithub上のリポジトリからご確認ください。


2.TouchEventの追加と物理空間の追加


タッチイベントの追加


// on "init" you need to initialize your instance
bool HelloWorld::init()
{
// 初期化に失敗したら
if(!Layer::init()) return false;

// タッチイベントを登録
EventListenerTouchOneByOne* listener { EventListenerTouchOneByOne::create() };
listener->onTouchBegan = CC_CALLBACK_2(HelloWorld::onTouchBegan, this);
listener->onTouchMoved = CC_CALLBACK_2(HelloWorld::onTouchMoved, this);
listener->onTouchEnded = CC_CALLBACK_2(HelloWorld::onTouchEnded, this);
this->getEventDispatcher()->addEventListenerWithSceneGraphPriority(listener, this);

// 物理空間の初期化
this->createPhysicsWorld();

return true;
}



物理空間を初期化する


// 物理空間の初期化
void HelloWorld::createPhysicsWorld()
{
// 重力ベクトル
b2Vec2 gravity {0,-9.8f};

// ワールドを重力を引数として初期化
b2World* world { new b2World(gravity) };
this->world = world;

world->SetAllowSleeping(true); // 動いていないオブジェクトは計算しないフラグを立てる(パフォーマンスのため)
world->SetContinuousPhysics(true);

// 壁を作る
const Size& size { this->getContentSize() };

b2BodyDef groundBodyDef;
groundBodyDef.position.Set(0, 0); // 画面の左下に基準点を置く
b2Body* groundBody = world->CreateBody(&groundBodyDef);
b2EdgeShape groundBox;

// 天井
groundBox.Set(b2Vec2{0,size.height/PTM_RATIO}, b2Vec2{size.width/PTM_RATIO,size.height/PTM_RATIO});
groundBody->CreateFixture(&groundBox,0);

// 左壁
groundBox.Set(b2Vec2{0,size.height/PTM_RATIO}, b2Vec2{0,0});
groundBody->CreateFixture(&groundBox,0);

// 右壁
groundBox.Set(b2Vec2{size.width/PTM_RATIO,size.height/PTM_RATIO}, b2Vec2{size.width/PTM_RATIO,0});
groundBody->CreateFixture(&groundBox,0);

// 地面
groundBox.Set(b2Vec2(0,0), b2Vec2{size.width/PTM_RATIO,0});
groundBody->CreateFixture(&groundBox,0);
}


b2Worldで重力ベクトル(0,-9.8f)で初期化しています。加えて、画面に壁を作っています。基本的には

素材の定義情報->実体化のような形になっています。親子関係ではBodyの上にShapeが載っている形をとっていて

これはCocos2d-xでも同じですね。

Box2Dで扱う座標系は画面上の座標系のままでは使えません。1mあたりを何ピクセルとして扱うか、を定義して使う必要があります。ここではヘッダーファイルで


HelloWorld.h


// 定数
public:
static constexpr float PTM_RATIO { 32.0f }; // Box2D上で1mを何ピクセルとして扱うか


と定義してあります。


3.update処理を追加する

まずinitに

   this->scheduleUpdate();

を追加して、毎フレーム呼ぶ処理を呼ぶようにしましょう


update処理


void HelloWorld::update(float dt)
{
// 端末のフレームにかかわらず60fpsでの計算の値で処理をさせる
this->world->Step(1/60.f, 1, 1, 1);
}

こうすることで空間の更新処理(置かれたものに重力を働く)が呼ばれるようになります。Stepの最初の引数の時間ですが

update(float dt)のdtを突っ込んでもよいですが、端末の処理によってまちまちになるので、フレームごとの計算では左が出ないように固有値(1/60.0f)を入れています

これで、Box2Dの基本的なシステムの設定は完了しました!


4.particleの導入

ここからがLiquidFunの機能を実装していくことになります。

LiquidFunでは、生成するSystem生成されたGroupが必要になります。

まず、粒子がどんな密度で生成するか、生成される粒子の半径はどうか、重力の効きやすさなどを定義する必要があります。


// パーティクルシステムの構築
b2ParticleSystemDef particleSystemDef;
particleSystemDef.density = 0.5;
particleSystemDef.radius = 10.0f / PTM_RATIO;
particleSystemDef.gravityScale = 3.0f;
this->particleSystem = world->CreateParticleSystem(&particleSystemDef);

このシステムを使い回すためメンバ変数に追加しておきます。

加えて、指定した場所に粒子群を作る処理を作りましょう

    //グループで生成する際はパーティクルグループの形を設定する必要がある

b2CircleShape* shape { new b2CircleShape() };
shape->m_radius = 50.0f / PTM_RATIO;

//パーティクルグループのデータ設定
b2ParticleGroupDef groupDef;

//パーティクルのグループを設定
groupDef.shape = shape;
groupDef.flags = b2_waterParticle | b2_particleContactListenerParticle;
groupDef.color = b2ParticleColor(137, 224, 253, 255);
groupDef.position.Set(position.x/PTM_RATIO,position.y/PTM_RATIO);

// グループを作成
b2ParticleGroup* particleGroup { this->particleSystem->CreateParticleGroup(groupDef) };

// 管理配列に追加
this->particleGroups.push_back(particleGroup);

b2CircleShapeは、粒子の集合が丸の形で作られます。その丸の中に粒子があるイメージです。大きめの50pxの半径にしてます。b2ParticleGroupDefでは、全体の形(shape),流体の挙動(flags),流体の色(color),位置(position)が決められます。flagsを調整することで、全体がゼリーのような挙動をさせたり、ねばねばしたものにさせたりすることができます。

あとの更新処理でparticleを配列に追加しています。

次に、物理エンジンでは、演算をするだけで、適当なテクスチャを与えなければ何も現れません。なので、パーティクルに紐付けてSpriteをつけてみましょう


void ** userData { particleSystem->GetUserDataBuffer() + particleGroup->GetBufferIndex() };

for(int i = 0; i < particleGroup->GetParticleCount();i++,userData++)
{
Sprite * water { Sprite::create("particle.png") };
water->getTexture()->setAliasTexParameters();
water->setScale((this->particleSystem->GetRadius() / (water->getContentSize().width / PTM_RATIO)));
(*userData) = water;
this->addChild(water);
}

Particleに紐づくポインタにSpriteを紐付けたあと、画面にaddChildしています。重力が効いているので、粒子'自身'(目には見えない)の位置は更新されますが、粒子に紐づけている画像の場所は別で管理してあげなければならないので、update処理を修正しましょう。


void HelloWorld::update(float dt)
{
// 端末のフレームにかかわらず60fpsでの計算の値で処理をさせる
this->world->Step(1/60.f, 1, 1, 1);

// 各パーティクルのデータから画像を更新する
for (b2ParticleGroup* group : this->particleGroups)
{

//グループのパーティクルを更新
void ** userData { particleSystem->GetUserDataBuffer() + group->GetBufferIndex() };

//座標バッファ
b2Vec2* vecList { particleSystem->GetPositionBuffer() + group->GetBufferIndex() };

//色バッファ
b2ParticleColor * colorList { particleSystem->GetColorBuffer() + group->GetBufferIndex() };

//バッファループ
for(int i = 0; i < group->GetParticleCount();i++,vecList++,userData++)
{
// 位置を更新
if(Sprite* particleImage { static_cast<Sprite*>(*userData) })
{
particleImage->setColor(Color3B((*colorList).r, (*colorList).g, (*colorList).b));
particleImage->setPosition((*vecList).x * PTM_RATIO, (*vecList).y*PTM_RATIO);
}
}
}

}

particleに設定していた情報から、Spriteの場所を更新しています。


5タッチした場所にパーティクルを追加するようにしよう


// タッチされたら
bool HelloWorld::onTouchBegan(Touch *touch, Event *event)
{

const Point& position { touch->getLocation() };
this->createParticle(position);
return true;
}

こんな感じになりました

ss.png

このように、物理エンジンとゲームエンジンでは描画を揃える処理をいれてあげることで、描画と演算が一致することがわかりました。Cocos2d-xのPhysicsBodyもそうですが、基本的には描画処理と演算処理を合わせる処理が多くを占めてるということが確認できます。

(単純な座標のみならず、回転も入ってくるため、Transformを適応するような処理になっていますが。)


ちなみに

今回作成したプロジェクトはこちらにあります!

参考になれば幸いです!