cocos2d-xで引っぱりゲーを作ってみる
いわゆる、ケリ姫スイーツとかAngry Birdsとかモンスターストライクとか、そういった類いの引っ張るアクションをcocos2d-xで試した時のメモ。
例によって、ただのメモなのでキレイに書こうとしてないとか、そもそもメソッドの作り方イケてないとかは多々あります。
プロジェクトは前回のエントリーで作ったHelloWorldAppを使い回し。
なので、すでに起動すればHelloWorldとボタンと画像が出てる初期状態のプロジェクトがあるものとして話しを進めます。
物理エンジン
cocos2d-xで使われてる物理エンジンとしては、Box2Dとchipmunkの2種類で、2系では結構Box2Dが使われていたのかな?と思いますが、3系ではchipmunkを強力にサポートするような感じになったみたいです。
例えば、下記のコード
auto scene = Scene::create();
これは初期状態のプロジェクトに書いてあるコード(HelloWorld.cpp的なファイルのcreateScene()メソッドにあるはずです。)ですが、これを下記のように書き直すと、、、
auto scene = Scene::createWithPhysics();
変更点としては、Scene::create()
をScene::createWithPhysics()
にしました。
これだけで、「このSceneは物理エンジンを使えるようにします。」という設定が出来てしまいます。
内部的にはchipmunkを使っているようです。
実装
実装といってもあまり難しいことはしません。
引っ張って飛ばす物体と地面を用意して、コードを書くだけです。
今回は、下記の2種類の画像を用意して使う事にしました。
sample_chara01.png
sample_floor.png
謎の生き物の方のセンスの無さの方はいったん置いといてください(^人^)
画像は適当に用意して、Resources
に入れてプロジェクトにAddしてください。
それでは、HelloWorldApp.cpp
(AppDeligateじゃない方の.cpp)をいじっていきます。
まずは、デフォルトで用意されてるinit()
メソッドを奇麗にします。
// on "init" you need to initialize your instance bool HelloWorld::init() { ////////////////////////////// // 1. super init first if ( !Layer::init() ) { return false; } return true; }
これだけ残して、それ以外は全部消しちゃいましょう。
続いて、createScene()
メソッドをいじっていきます。
まずは、auto scene = Scene::create();
を下記に書き換えます。
auto scene = Scene::createWithPhysics();
続いて、return scene
の手前に以下のコードを書きます。
Size winSize = Director::getInstance()->getWinSize(); // 重力を設定 PhysicsWorld* world = scene->getPhysicsWorld(); Vect gravity; gravity.setPoint(0, -150); world->setGravity(gravity); // 地面 Sprite* floor = Sprite::create("sample_floor.png"); floor->setPosition(Point(winSize.width / 2 , floor->getContentSize().height / 2)); PhysicsBody* floorPb = PhysicsBody::createBox(floor->getContentSize()); floorPb->setDynamic(false); floor->setPhysicsBody(floorPb); layer->addChild(floor); // キャラクター Sprite* character = Sprite::create("sample_chara01.png"); character->setPosition(Point(winSize.width / 5 , winSize.height / 2)); PhysicsBody* charaPb = PhysicsBody::createCircle(40); charaPb->setMass(1.0f); // 重さを指定(ここが無いと後で飛ばせなくなる) character->setPhysicsBody(charaPb); character->setTag(1); layer->addChild(character);
一つずつ見て行きましょう。
Director::getInstance()->getWinSize();
ここでは画面のサイズを取得しています。
cocos2d-xの2系ではsharedDirector
を使っていたのですが、3系ではgetInstance
になった様です。
何度か素通りしてしまっていますが、CCDirector
などCC
のプレフィックスが付かなくなったのも大きい変更点です。
続いて、シーンに紐づく物理世界に重力(っぽいもの)を与えます。
gravity.setPoint(0, -150);
このコードですが、ここのPointの(x, y)に相当するのものは座標ではなく力の方向です。
Point(100, 50)
とした場合、x座標:100、y座標:50の場所が引力の中心になるという設定ではなく、x方向に100、y方向に50の力が重力っぽく加わりますよ、という設定です。
次に地面を作ります。
だいたいは読んで字のごとくなコードなのですが、下記の一文だけは押さえておいたほうが良さげです。
floorPb->setDynamic(false);
「これは静止した状態の、物理的な性質をもった物体ですよ」という設定です。
つまり、重力によって移動することがなく、何かとぶつかっても反発することがなくなる、という設定です。
今回、地面に勝手に動かれては困るのでこうしました。
次にキャラクターを作ります。
あとで参照するためにタグを設定したり、引っ張るアクションの時に重さが無いと動いてくれないので重さ(mass)を設定したりしています。
ここまで書き終えたところで、いったんシミュレータで実行してみると、空中にキャラクターが現れて、それが重力によって自由落下し、地面にぶつかってバウンドして静止したかと思います。
ちょうどこんな状態になりました。
ここまでで大枠はできたので、続いてメインの引っ張るアクションの実装をしていきます。
今度は新しくメソッドを用意しないといけないので、HelloWorldApp.h
を開きます。
既にあるメソッド群の最後に以下を付け足しました。
// Touchイベントの開始地点 cocos2d::Point touchPoint; // Touchイベント用 bool onTouchBegan(cocos2d::Touch* touch, cocos2d::Event* event); void onTouchEnded(cocos2d::Touch* touch, cocos2d::Event* event); // NodeのRectを取得する cocos2d::Rect getRect(cocos2d::Node* node);
そしたらHelloWorldApp.cpp
の方に戻り、先ほどのcreateScene()
のreturn scene
の手前に以下を追記します。(キャラクター作成などの続きのところです。)
// Touchイベント用 auto eventDispatcher = Director::getInstance()->getEventDispatcher(); auto listener = EventListenerTouchOneByOne::create(); listener->onTouchBegan = CC_CALLBACK_2(HelloWorld::onTouchBegan, layer); listener->onTouchEnded = CC_CALLBACK_2(HelloWorld::onTouchEnded, layer); eventDispatcher->addEventListenerWithSceneGraphPriority(listener, layer);
ここではタップイベントによって呼ばれるメソッドを定義しています。
これ以外にもOnTouchMoved
やonTouchCanceled
などがあるのですが、今回は「タップした時」と「離した時」だけで十分なので使わない事にしました。
続いて、これらのメソッドを実装していきます。
Rect HelloWorld::getRect(Node* node) { Point point = node->getPosition(); int width = node->getContentSize().width; int height = node->getContentSize().height; return Rect(point.x - (width / 2), point.y - (height / 2), width, height); } bool HelloWorld::onTouchBegan(Touch* touch, Event* event) { Sprite* character = (Sprite*)this->getChildByTag(1); Rect characterRect = getRect(character); touchPoint = touch->getLocation(); return characterRect.containsPoint(touchPoint); } void HelloWorld::onTouchEnded(Touch* touch, Event* event) { Sprite* character = (Sprite*)this->getChildByTag(1); Point endPoint = touch->getLocation(); Vect force = Vect(touchPoint.x - endPoint.x, touchPoint.y - endPoint.y) * 4; character->getPhysicsBody()->applyImpulse(force); }
getRect()
は、引数のcocos2d::Node
(を継承している)オブジェクトのRectを取得して返却するものです。
あとで、タップされた時にキャラクターをタップしたかどうかの判定の時に使います。
onTouchBegan()
では、タップされた瞬間にキャラクターがタップされたかどうかを判定します。
ここでポイントとなるのがreturn
の部分なのですが、ここでtrue
を返さないと後続のメソッドonTouchEnded()
などが呼ばれないという点です。
つまり、ここでキャラクターがタップされたか否かを返せば、onTouchEnded()
が呼ばれるのはキャラクターがタップされた場合のタップ終了時のみに制限できます。
便利ですね!
最後にonTouchEnded
では、タップされた地点の座標
からタップ終了時の地点の座標
を引いたベクトルを力としてキャラクターに与えています。
* 4
してるのは、そのままだとあんまり飛ばなかったのでちょっと補正をかけてあげただけで機能的な意味は特にないです。
完成
これで一通り出来たので実際に起動してみます。
完成したものを動かしたら下記のような感じになりました。
すげー分かりづらいですが、キャラをクリックして、そのまま引っぱり離しています。
ケリ姫やAngry Birds的にはこんな感じでしょうか。
これにスキルだの放物線ガイドだのを付け足すとそれっぽくなるかなぁ、という感じです。
てかcocos2d-xもWebPlayer欲しいです。。。
伝えにくいですねー><
最後に、今回いじったプログラムを乗っけておきます。
HelloWorldApp.h
#ifndef __HELLOWORLD_SCENE_H__ #define __HELLOWORLD_SCENE_H__ #include "cocos2d.h" class HelloWorld : public cocos2d::Layer { public: // there's no 'id' in cpp, so we recommend returning the class instance pointer static cocos2d::Scene* createScene(); // Here's a difference. Method 'init' in cocos2d-x returns bool, instead of returning 'id' in cocos2d-iphone virtual bool init(); // a selector callback void menuCloseCallback(cocos2d::Ref* pSender); // implement the "static create()" method manually CREATE_FUNC(HelloWorld); /** * 以下を追加 */ // Touchイベントの開始地点 cocos2d::Point touchPoint; // Touchイベント用 bool onTouchBegan(cocos2d::Touch* touch, cocos2d::Event* event); void onTouchEnded(cocos2d::Touch* touch, cocos2d::Event* event); // NodeのRectを取得する cocos2d::Rect getRect(cocos2d::Node* node); }; #endif // __HELLOWORLD_SCENE_H__
HelloWorldApp.cpp
#include "HelloWorldScene.h" USING_NS_CC; Scene* HelloWorld::createScene() { // 'scene' is an autorelease object auto scene = Scene::createWithPhysics(); // 'layer' is an autorelease object auto layer = HelloWorld::create(); // add layer as a child to scene scene->addChild(layer); /** * 以下を追加 */ Size winSize = Director::getInstance()->getWinSize(); // 重力を設定 PhysicsWorld* world = scene->getPhysicsWorld(); Vect gravity; gravity.setPoint(0, -150); world->setGravity(gravity); // 地面 Sprite* floor = Sprite::create("sample_floor.png"); floor->setPosition(Point(winSize.width / 2 , floor->getContentSize().height / 2)); PhysicsBody* floorPb = PhysicsBody::createBox(floor->getContentSize()); floorPb->setDynamic(false); floor->setPhysicsBody(floorPb); layer->addChild(floor); // キャラクター Sprite* character = Sprite::create("sample_chara01.png"); character->setPosition(Point(winSize.width / 5 , winSize.height / 2)); PhysicsBody* charaPb = PhysicsBody::createCircle(40); charaPb->setMass(1.0f); // 重さを指定(ここが無いと後で飛ばせなくなる) character->setPhysicsBody(charaPb); character->setTag(1); layer->addChild(character); // Touchイベント用 auto eventDispatcher = Director::getInstance()->getEventDispatcher(); auto listener = EventListenerTouchOneByOne::create(); listener->onTouchBegan = CC_CALLBACK_2(HelloWorld::onTouchBegan, layer); listener->onTouchEnded = CC_CALLBACK_2(HelloWorld::onTouchEnded, layer); eventDispatcher->addEventListenerWithSceneGraphPriority(listener, layer); // return the scene return scene; } Rect HelloWorld::getRect(Node* node) { Point point = node->getPosition(); int width = node->getContentSize().width; int height = node->getContentSize().height; return Rect(point.x - (width / 2), point.y - (height / 2), width, height); } bool HelloWorld::onTouchBegan(Touch* touch, Event* event) { Sprite* character = (Sprite*)this->getChildByTag(1); Rect characterRect = getRect(character); touchPoint = touch->getLocation(); return characterRect.containsPoint(touchPoint); } void HelloWorld::onTouchEnded(Touch* touch, Event* event) { Sprite* character = (Sprite*)this->getChildByTag(1); Point endPoint = touch->getLocation(); Vect force = Vect(touchPoint.x - endPoint.x, touchPoint.y - endPoint.y) * 4; character->getPhysicsBody()->applyImpulse(force); } // on "init" you need to initialize your instance bool HelloWorld::init() { ////////////////////////////// // 1. super init first if ( !Layer::init() ) { return false; } return true; } void HelloWorld::menuCloseCallback(Ref* pSender) { Director::getInstance()->end(); #if (CC_TARGET_PLATFORM == CC_PLATFORM_IOS) exit(0); #endif }