読者です 読者をやめる 読者になる 読者になる

ことばアルバム

にわかエンジニアのにわか備忘録

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

f:id:is4cafe:20140322001605p:plain

 
sample_floor.png

f:id:is4cafe:20140322001623p:plain

謎の生き物の方のセンスの無さの方はいったん置いといてください(^人^)
画像は適当に用意して、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)を設定したりしています。

 
ここまで書き終えたところで、いったんシミュレータで実行してみると、空中にキャラクターが現れて、それが重力によって自由落下し、地面にぶつかってバウンドして静止したかと思います。

f:id:is4cafe:20140322003858p:plain

ちょうどこんな状態になりました。

 
ここまでで大枠はできたので、続いてメインの引っ張るアクションの実装をしていきます。


今度は新しくメソッドを用意しないといけないので、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);

ここではタップイベントによって呼ばれるメソッドを定義しています。
これ以外にもOnTouchMovedonTouchCanceledなどがあるのですが、今回は「タップした時」と「離した時」だけで十分なので使わない事にしました。
 
続いて、これらのメソッドを実装していきます。

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してるのは、そのままだとあんまり飛ばなかったのでちょっと補正をかけてあげただけで機能的な意味は特にないです。


完成

これで一通り出来たので実際に起動してみます。
完成したものを動かしたら下記のような感じになりました。

f:id:is4cafe:20140322010147g:plain

すげー分かりづらいですが、キャラをクリックして、そのまま引っぱり離しています。
ケリ姫や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
}