Cocos2d-xで文字送りを実装する

RPGやノベルで表示されるテキストが1文字ずつ表示されていく、「文字送り」機能を作成したいと思います。

画面の構成は、周りの枠をScale9Spriteで表示して、テキストはラベルで表示します。図にするとこんな感じです。

f:id:Takachan:20170905222419p:plain

おおよその仕様ですが以下の通りです。

  • テキストがテキスト枠内に表示される
  • 文字は1文字ずつ時間を順番に表示されていく
  • 文字送り中にタップすると残りの文字が一括で表示される
  • 1つの文章が表示し終わったら矢印が点滅する

で、出来上がったのがこちら。

実装

今回コードがかなり長くなってしまったので全体はGithubにアップしました。

なのでヘッダーと基本的な考え方、使い方を紹介します。

ヘッダー

public部分のみ抜粋しています。高さと横幅を指定してcreateメソッドを呼び出して、addMessageで表示するメッセージを指定し、start() → next()の順に呼び出します。

処理の内容ですが文字送りはMessageDialogのupdateメソッド内でインターバルを過ぎたら1文字表示(最初は文字全体を透明にしておいて、1文字ずつ不透明に変更)しています。また、文字が最後まで表示されたときに表示される矢印はアニメーションでこちらも透明度を透明 → 不透明で点滅を表現しています。

#pragma once

#include "cocos2d.h"
#include "ui\UIScale9Sprite.h"

class MessageDialog : public cocos2d::Node
{
    // ...省略...

public:
    
    MessageDialog() { /* nop */ }
    ~MessageDialog();

    virtual bool init(const int frameWidth, const int frameHeight);
    static MessageDialog* create(const int frameWidth, const int frameHeight);

    // 表示する文字を追加する
    void addMessage(const std::string &message);

    // 文字送りを開始する
    void start();

    // 次の動作(残りを全部一括で表示、次のパラグラフ)
    void next();

    // ダイアログの内容が全部読みお終わったときに呼ばれるコールバックの設定
    void setCompleteAction(std::function<void()> completedAction);

    // サイズの変更
    void setContentSize(const cocos2d::Size& contentSize) override;
};

使い方

HelloWorldにダイアログを直接追加しています。全部のメッセージの表示が最後まで終了するとsetCompleteAction()に設定したラムダ式が呼び出されるので終了時の処理を記述しています。

画面をTouchするごとにnext()メソッドを呼び出して、次の文書の表示、もしくは文字送り中の文書を最後まで一括表示しています。(今気が付きましたがstart()メソッド呼んでないのに普通に動いてますね…)

bool HelloWorld::init()
{
    // ...省略...
    auto listner = EventListenerTouchOneByOne::create();
    listner->onTouchBegan = [this](Touch* touch, Event* event)
    {
        if (this->dialog == nullptr)
        {
            this->dialog = MessageDialog::create(350, 100);
            this->dialog->addMessage("こんにちは。");
            this->dialog->addMessage("これはテストメッセージです。");
            this->dialog->addMessage("このような長いテキストは折り返して表示します。");
            this->dialog->addMessage("文字が長すぎるとはみ出すのでテキストを調整します。");
            this->dialog->addMessage("文字送りは途中でスキップできます。");

            this->dialog->setCompleteAction([this]()
            {
                this->dialog->runAction(
                    Sequence::create(
                        ScaleTo::create(0.1f, 0, 0.05f, 1),
                        ScaleTo::create(0.1f, 1, 0.05f, 0.05f),
                        RemoveSelf::create(true), nullptr));
            });

            auto _vsize = Director::getInstance()->getVisibleSize();

            this->dialog->setPosition(_vsize.width/2, _vsize.height/2);
            this->dialog->setAnchorPoint(Vec2::ANCHOR_TOP_LEFT);

            this->addChild(this->dialog);

            this->dialog->setScale(0.05f);
            this->dialog->runAction(Sequence::create(ScaleTo::create(0.1f, 0, 1, 1), 
                                    ScaleTo::create(0.1f, 1, 1, 1), nullptr));
        }
        else
        {
            this->dialog->next();
        }
        
        return true;
    };

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

    return true;

さいごに

説明のためにかなり単純化しています。このため、汎用性と拡張性が全然ないので実際にどこかに組み込むときはこれをベースに色々機能を追加することになると思います。