おつかれさまです。藤澤です。
前回のブログのときには事前登録中だったクラッシュフィーバーですが、おかげさまで多くの皆さまにプレイしていただいております。皆さまありがとうございます。また、いろいろと不具合でご迷惑をおかけしており誠に恐れ入ります。不具合修正に加えて機能追加も鋭意行なっておりますので、いましばらくお待ちいただければ幸いです。
さて今回は、そのクラッシュフィーバーでも使用しておりますカルーセルの作り方をご紹介したいと思います。
考え方は簡単です。
- Node を円形に配置する。
- y 軸方向の半径を縮めて楕円にする。
- 手前の Node を大きくし、奥の Node を小さく、半透明にするとそれっぽく見えるはず。
では実際にやってみましょう。
- まず適当に 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)); }
- y 軸方向を潰してみます。
float y = RADIUS * sin(radians) * FLATTEN_RATE;
- 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); } }