Cocos2d-xでロマンシングサ・ガ3風のダメージ表示を再現する

Cocos2d-x(3.x系)で、ロマンシングサ・ガ3風のダメージ表示のエフェクトを再現してみました。

制作環境は以下の通りです。

  • Cocos2d-x 3.14.1
  • Windows10上でWin32プロジェクトにて確認

参考にした動画

www.youtube.com

実装した結果

微妙に画像がつぶれているので見づらい場合は拡大してください。

説明

当方観察力が無いので、オリジナルの動画から仕様が読み取れているか謎ですが、以下のような感じだと思います。

  • 各桁は放物線を描くように放り投げられるように表示される
  • 敵のダメージ表示は最終桁 → 先頭桁の順に数字が大きく放り投げられて着地する
  • 味方の場合先頭桁 → 最終桁の順に数字が大きく放り投げられて着地する
  • 着地した瞬間ほんの少しだけバウンドする

また、実装時は考慮していませんが

  • 数字の初期位置が各桁で少し縦にずれている
  • 最終桁だけ放物線が大きい?

などの効果が入っているかもしれません。

で、実装の方針ですが

  • Nodeを継承した自作クラスを作成する
  • 複数のLabelをNodeでまとめて管理する

で行きたいと思います。

ヘッダー側

Nodeを継承した表示用のクラスをこんな感じで用意します。

#pragma once
#pragma execution_character_set("utf-8")

#include "cocos2d.h"

// ダメージ表示を表します。
class DmgEffect : public cocos2d::Layer
{
    // 左向きに数値を表示するかどうかのフラグ(デフォは右)
    bool _isLeft = false;

    DmgEffect();

    // 各桁を表すラベルを作成する
    void createLabels(std::string dmg);

    // 最後に自殺する動作を設定する
    void setupCleanupAction(std::string dmg, float dulation);

    // 生成したラベルを取り出す
    std::vector<Node*> getLabels(std::string dmg);

public:

    // リソースを解放してオブジェクトを破棄する
    ~DmgEffect();

    // いつもの
    virtual bool init() override;
    
    // いつもの
    static DmgEffect* create();

    // 指定した数値でダメージエフェクトを開始する
    void showEffect(int dmg);

    // 全てのActionが終了した時の処理
    void remove();

    // 右か左かの設定または取得
    void isLeft(bool value) { this->_isLeft = value; }
    bool isLeft() { return this->_isLeft; }
};

実装側(抜粋)

上記ヘッダーに肉付けしていきます。表示状態は管理しないので、全部終了したら自分で自分を消去するように仕込んでおきます。(そのためにNodeを継承しています)

#include "DmgEffect.h"

using namespace std;
using namespace cocos2d;

DmgEffect::DmgEffect()
{
    // nop
}

void DmgEffect::createLabels(string dmg)
{
    for (int i = 0; i < dmg.length(); i++)
    {
        // BABARAGEOさんからお借りしたBABARAGEOフォント
        // http://babarageo.com/cgi/wp/ja/notes/babarageo-font.html
        auto _label = Label::create(std::string{ dmg[i] }, "fonts/babarageo3.ttf", 10);
        _label->setTag(i);
        this->addChild(_label);
    }
}
void DmgEffect::setupCleanupAction(string dmg, float dulation)
{
    // エフェクトが全部終わったら自殺する設定
    Node* last = this->getChildByTag(dmg.length()-1);
    last->runAction(Sequence::create(DelayTime::create(dulation + 0.3f),
        CallFunc::create(CC_CALLBACK_0(DmgEffect::remove, this)), nullptr));
}

std::vector<Node*> DmgEffect::getLabels(string dmg)
{
    vector<Node*> labelList;
    for (int i = 0; i < dmg.length(); i++)
    {
        labelList.push_back(this->getChildByTag(i));
    }

    if (this->_isLeft)
    {
        std::reverse(std::begin(labelList), std::end(labelList));
    }

    return labelList;
}

DmgEffect::~DmgEffect()
{
    this->remove();
}

bool DmgEffect::init() { /* 省略 */ }

DmgEffect* DmgEffect::create() { /* 省略 */ }

void DmgEffect::showEffect(int dmg)
{
    string dmgStr = to_string(dmg);

    this->createLabels(dmgStr);

    // 数字がジャンプする動作の設定
    int i = 0.1;
    float dulation = 0.0f;
    for (Node* _label : this->getLabels(dmgStr))
    {
        dulation = i / 18.0f;
        Vec2 newPos(14 * i * (this->_isLeft ? -1 : 1), 0);
        auto jumpAction1 = JumpBy::create(dulation, newPos, 7 * i, 1);
        
        auto jumpAction2 = JumpBy::create(0.07f, Vec2(0, 0), 5, 1); // バウンド

        _label->runAction(Sequence::create(jumpAction1, jumpAction2, nullptr));
        i++;
    }

    this->setupCleanupAction(dmgStr, dulation);
}

void DmgEffect::remove()
{
    this->removeAllChildrenWithCleanup(true);
    this->removeFromParentAndCleanup(true);
}

利用側コード

使用する際にエフェクト表示前に右側・左側のプロパティを指定して、タッチ座標にエフェクトを表示しています。

bool HelloWorld::init()
{
    // ...省略...

    srand((unsigned int)time(nullptr));

    auto listner = EventListenerTouchOneByOne::create();

    //Sprite* sp = this->animSp;

    listner->onTouchBegan = [this](Touch* touch, Event* event)
    {
        DmgEffect* effect = DmgEffect::create();
        effect->isLeft((this->cnt++ %2 == 0 ? true : false)); // 左右に振り分け
        effect->setPosition(touch->getLocation());
        effect->showEffect(rand() % 9999);
        this->addChild(effect);

        return true;
    };

    this->getEventDispatcher()->addEventListenerWithSceneGraphPriority(listner, this);

    return true;
}

処理手順ですが

  1. ダメージを数字で取る
  2. 各桁に対応する文字をラベルで生成する
  3. ラベルにジャンプアクション(JunbBy)を設定する

で、

  1. 偶数回タップ時は左(敵のダメージ表現)、奇数回タップ時は右(味方ダメージ表現)

としています。

数字が放物線を描いて着地 → 少しバウンド、の動作はJumpアクションをSequenceで順番に実行しています。

Jumpアクションを使うと放物線の表現は簡単にできます。但し全て等速運動なのでご注意ください。

最後に - 汎用化の考察

「複数のラベルを作成して画面に追加」して「終了後に全部まとめて消す」というパターンは結構よくありそうです。そこで、showEffect関数内の「// 数字がジャンプする動作の設定」の個所をデリゲーションして外から具体的な動作を注入したり、派生クラスで動きをつけたり拡張できそうなので、そういったエフェクトならばダメージ以外以外にも適用できそうです。

関連記事

Cocos2d-xでロマンシングサ・ガ3風のダメージ表示を再現する(この記事) Cocos2d-xでFF5風のダメージを再現する