エンジニア

Cocos2d-x でカルーセルを作る

投稿日:2015年9月9日 更新日:

おつかれさまです。藤澤です。

前回のブログのときには事前登録中だったクラッシュフィーバーですが、おかげさまで多くの皆さまにプレイしていただいております。皆さまありがとうございます。また、いろいろと不具合でご迷惑をおかけしており誠に恐れ入ります。不具合修正に加えて機能追加も鋭意行なっておりますので、いましばらくお待ちいただければ幸いです。

さて今回は、そのクラッシュフィーバーでも使用しておりますカルーセルの作り方をご紹介したいと思います。

考え方は簡単です。

  1. Node を円形に配置する。
  2. y 軸方向の半径を縮めて楕円にする。
  3. 手前の Node を大きくし、奥の Node を小さく、半透明にするとそれっぽく見えるはず。

では実際にやってみましょう。

  1. まず適当に Node を作り、円周上に等間隔に配置します。
    Vector<Node *> items;
    items.pushBack(Label::createWithSystemFont("1", "Arial", 72));
    items.pushBack(Label::createWithSystemFont("2", "Arial", 72));
    items.pushBack(Label::createWithSystemFont("3", "Arial", 72));
    items.pushBack(Label::createWithSystemFont("4", "Arial", 72));
    items.pushBack(Label::createWithSystemFont("5", "Arial", 72));

    for (auto item : items)
        this->addChild(item);

    float theta = 360.0f / items.size();
    for (int i = 0; i < items.size(); i++)
    {
        // 270 度の位置が正面にくるように  
        float angle = theta * i + 270.0f;
        float radians = angle * PI / 180.0f;
        float x = RADIUS * cos(radians);
        float y = RADIUS * sin(radians);
        items.at(i)->setPosition(Vec2(x, y));
    }

  1. y 軸方向を潰してみます。
        float y = RADIUS * sin(radians) * FLATTEN_RATE;

  1. y 座標に応じて scale と opacity を調整します。ついでに手前の Node の Z オーダが大きくなるようにしておきます。
    float theta = 360.0f / items.size();
    for (int i = 0; i < items.size(); i++)
    {
        // 270 度の位置が正面にくるように  
        float angle = theta * i + 270.0f;
        float radians = angle * PI / 180.0f;
        float x = RADIUS * cos(radians);
        float y = RADIUS * sin(radians) * FLATTEN_RATE;
        float radiusY = RADIUS * FLATTEN_RATE;
        float diameterY = radiusY * 2;
        float scale = (diameterY - y) / diameterY;
        GLubyte opacity = 255 - (y + radiusY);
        items.at(i)->setPosition(Vec2(x, y));
        items.at(i)->setScale(scale);
        items.at(i)->setOpacity(opacity);
        items.at(i)->setZOrder(diameterY - y);
    }

それっぽくなりました。
次にこれを動かしてみたいと思います。
これも考え方はシンプルで、スワイプにあわせて回転角度を変更するだけです。

bool Carousel::init()
{
    if (!Node::init())
        return false;

    this->items.clear();
    this->items.pushBack(Label::createWithSystemFont("1", "Arial", 72));
    this->items.pushBack(Label::createWithSystemFont("2", "Arial", 72));
    this->items.pushBack(Label::createWithSystemFont("3", "Arial", 72));
    this->items.pushBack(Label::createWithSystemFont("4", "Arial", 72));
    this->items.pushBack(Label::createWithSystemFont("5", "Arial", 72));

    for (auto item : items)
        this->addChild(item);

    this->angle = 0.0f;
    this->arrange();

    EventListenerTouchOneByOne *listener = EventListenerTouchOneByOne::create();
    listener->onTouchBegan = [](Touch *touch, Event *event){ return true; };
    listener->onTouchMoved = [&](Touch *touch, Event *event){
        float delta = touch->getLocation().x - touch->getPreviousLocation().x;
        this->angle += delta;
        this->arrange();
    };
    this->getEventDispatcher()->addEventListenerWithSceneGraphPriority(listener, this);

    return true;
}

void Carousel::arrange()
{
    float theta = 360.0f / items.size();
    float baseAngle = this->angle + 270.0f;
    for (int i = 0; i < items.size(); i++)
    {
        // 270 度の位置が正面にくるように  
        float angle = theta * i + baseAngle;
        float radians = angle * PI / 180.0f;
        float x = RADIUS * cos(radians);
        float y = RADIUS * sin(radians) * FLATTEN_RATE;
        float radiusY = RADIUS * FLATTEN_RATE;
        float diameterY = radiusY * 2;
        float scale = (diameterY - y) / diameterY;
        GLubyte opacity = 255 - (y + radiusY);
        this->items.at(i)->setPosition(Vec2(x, y));
        this->items.at(i)->setScale(scale);
        this->items.at(i)->setOpacity(opacity);
        this->items.at(i)->setZOrder(diameterY - y);
    }
}

最後に、指を離したら勝手にホームポジションに戻るようにしてみたいと思います。
ScrollView の実装を参考に、所定の位置にくるまで再帰的に移動させます。

    EventListenerTouchOneByOne *listener = EventListenerTouchOneByOne::create();
    listener->onTouchBegan = [](Touch *touch, Event *event){ return true; };
    listener->onTouchMoved = [&](Touch *touch, Event *event){
        float delta = touch->getLocation().x - touch->getPreviousLocation().x;
        this->angle += delta;
        this->arrange();
    };
    listener->onTouchEnded = [&](Touch *touch, Event *event){
        // Z オーダが一番大きい(= 一番手前の)オブジェクトを探す  
        int maxZ = 0;
        ssize_t frontIndex = 0;
        for (int i = 0; i < this->items.size(); i++)
        {
            if (this->items.at(i)->getZOrder() > maxZ)
            {
                maxZ = this->items.at(i)->getZOrder();
                frontIndex = i;
            }
        }

        float theta = 360.0f / this->items.size();
        float angleToMove = 360.0f - theta * frontIndex;

        // 逆回転防止  
        if (this->angle > 180.0f) this->angle -= 360.0f;
        if (this->angle < -180.0f) this->angle += 360.0f;
        if (angleToMove - this->angle > 180.0f) angleToMove -= 360.0f;
        if (angleToMove - this->angle < -180.0f) angleToMove += 360.0f;

        auto moveToHome = [=](float){
            if (fabs(angleToMove - this->angle) <= 0.1f)
            {
                this->unschedule("moveToHome");
                this->angle = angleToMove;
                this->arrange();
                return;
            }

            float delta = (angleToMove - this->angle) * 0.1f;
            this->angle += delta;
            this->arrange();
        };
        this->schedule(moveToHome, 0.0f, "moveToHome");
    };
    this->getEventDispatcher()->addEventListenerWithSceneGraphPriority(listener, this);

それっぽく動くようになりました。
実際のプロダクトではスムーズに動くようにいろいろと調整をしておりますが、基本的な実装としてはこれだけです。意外と簡単ですよね。

今回のコードの全体を載せておきます。

#include "cocos2d.h"

class Carousel : public cocos2d::Node
{
private:
    cocos2d::Vector<cocos2d::Node *> items;
    float angle;

    bool init();

    void arrange();

public:
    static Carousel* create();

};
#include "Carousel.h"

using namespace cocos2d;

#define PI 3.14159265359f  
#define RADIUS 100  
#define FLATTEN_RATE 0.4f  

Carousel* Carousel::create()
{
    Carousel *instance = new Carousel();
    instance->init();
    instance->autorelease();
    return instance;
}

bool Carousel::init()
{
    if (!Node::init())
        return false;

    this->items.clear();
    this->items.pushBack(Label::createWithSystemFont("1", "Arial", 72));
    this->items.pushBack(Label::createWithSystemFont("2", "Arial", 72));
    this->items.pushBack(Label::createWithSystemFont("3", "Arial", 72));
    this->items.pushBack(Label::createWithSystemFont("4", "Arial", 72));
    this->items.pushBack(Label::createWithSystemFont("5", "Arial", 72));

    for (auto item : items)
        this->addChild(item);

    this->angle = 0.0f;
    this->arrange();

    EventListenerTouchOneByOne *listener = EventListenerTouchOneByOne::create();
    listener->onTouchBegan = [](Touch *touch, Event *event){ return true; };
    listener->onTouchMoved = [&](Touch *touch, Event *event){
        float delta = touch->getLocation().x - touch->getPreviousLocation().x;
        this->angle += delta;
        this->arrange();
    };
    listener->onTouchEnded = [&](Touch *touch, Event *event){
        // Z オーダが一番大きい(= 一番手前の)オブジェクトを探す  
        int maxZ = 0;
        ssize_t frontIndex = 0;
        for (int i = 0; i < this->items.size(); i++)
        {
            if (this->items.at(i)->getZOrder() > maxZ)
            {
                maxZ = this->items.at(i)->getZOrder();
                frontIndex = i;
            }
        }

        float theta = 360.0f / this->items.size();
        float angleToMove = 360.0f - theta * frontIndex;

        // 逆回転防止  
        if (this->angle > 180.0f) this->angle -= 360.0f;
        if (this->angle < -180.0f) this->angle += 360.0f;
        if (angleToMove - this->angle > 180.0f) angleToMove -= 360.0f;
        if (angleToMove - this->angle < -180.0f) angleToMove += 360.0f;

        auto moveToHome = [=](float){
            if (fabs(angleToMove - this->angle) <= 0.1f)
            {
                this->unschedule("moveToHome");
                this->angle = angleToMove;
                this->arrange();
                return;
            }

            float delta = (angleToMove - this->angle) * 0.1f;
            this->angle += delta;
            this->arrange();
        };
        this->schedule(moveToHome, 0.0f, "moveToHome");
    };
    this->getEventDispatcher()->addEventListenerWithSceneGraphPriority(listener, this);

    return true;
}

void Carousel::arrange()
{
    float theta = 360.0f / items.size();
    float baseAngle = this->angle + 270.0f;
    for (int i = 0; i < items.size(); i++)
    {
        // 270 度の位置が正面にくるように  
        float angle = theta * i + baseAngle;
        float radians = angle * PI / 180.0f;
        float x = RADIUS * cos(radians);
        float y = RADIUS * sin(radians) * FLATTEN_RATE;
        float radiusY = RADIUS * FLATTEN_RATE;
        float diameterY = radiusY * 2;
        float scale = (diameterY - y) / diameterY;
        GLubyte opacity = 255 - (y + radiusY);
        this->items.at(i)->setPosition(Vec2(x, y));
        this->items.at(i)->setScale(scale);
        this->items.at(i)->setOpacity(opacity);
        this->items.at(i)->setZOrder(diameterY - y);
    }
}

採用情報

ワンダープラネットでは、一緒に働く仲間を幅広い職種で募集しております。

-エンジニア
-

© WonderPlanet Inc.