10
3

More than 3 years have passed since last update.

【C++/OpenSiv3D】シーン切り替え時のエフェクトをカスタマイズする

Posted at

OpenSiv3D の SceneManager を使うと、シーン遷移時に自動でフェードイン/アウトのエフェクトが入ります。このときの挙動をカスタマイズするには、drawFadeIn(double t) const overridedrawFadeOut(double t) const override を実装します。この記事では、シーン切り替え時のエフェクトのカスタマイズ例を紹介します。(対象: OpenSiv3D v0.4.2~)

時計

時計のように回転しながら背景色に切り替わります。
SceneTransition1.gif

namespace SceneTransition
{
    class Clock
    {
    private:

        ColorF m_backgroundColor = ColorF(0.5, 0.8, 1.0);

    public:

        void fadeIn(double t)
        {
            const double r = Scene::Center().length();
            Circle(Scene::Center(), r).drawPie(0_deg, -(1.0 - t) * 360_deg, m_backgroundColor);
        }

        void fadeOut(double t)
        {
            const double r = Scene::Center().length();
            Circle(Scene::Center(), r).drawPie(0_deg, t * 360_deg, m_backgroundColor);
        }
    };
}

ステンシル

キャラクターのシルエットを使って背景色に切り替わります。
SceneTransition2.gif

namespace SceneTransition
{
    class Stencil
    {
    private:

        Texture m_texture;

        RenderTexture m_screenTexture;

        ColorF m_backgroundColor = ColorF(0.8, 0.4, 0.0);

        void fade(double t)
        {
            m_screenTexture.clear(m_backgroundColor);
            {
                ScopedRenderTarget2D rt(m_screenTexture);
                BlendState b;
                b.writeR = b.writeG = b.writeB = false;
                b.srcAlpha = b.dstAlpha = Blend::One;
                b.opAlpha = BlendOp::RevSubtract;
                ScopedRenderStates2D blend(b);

                m_texture.scaled(EaseInSine(t) * 8.0).drawAt(Scene::Center());
            }

            m_screenTexture.draw();
        }

    public:

        Stencil()
            : m_screenTexture(Scene::Size())
        {
            m_texture = Texture(Emoji(U"🦊"));
        }

        void fadeIn(double t)
        {
            fade(t);
        }

        void fadeOut(double t)
        {
            fade(1.0 - t);
        }
    };
}

使い方

  1. SceneManager<State, Data> の共通データ Data に、上記クラスを sceneTransition という名前で追加します。

  2. 各シーンで drawFadeIn(double t) const overridedrawFadeOut(double t) const override を実装し、その中で draw() を呼んだあとに sceneTransition.fadeIn(t) または sceneTransition.fadeOut(t) を呼びます。

  3. シーン遷移時間を changeScene() の第二引数で適宜調整します。

使い方サンプル

シーン遷移のサンプル に、今回のエフェクトを組み込みます。

# include <Siv3D.hpp>

namespace SceneTransition
{
    class Clock
    {
    private:

        ColorF m_backgroundColor = ColorF(0.5, 0.8, 1.0);

    public:

        void fadeIn(double t)
        {
            const double r = Scene::Center().length();
            Circle(Scene::Center(), r).drawPie(0_deg, -(1.0 - t) * 360_deg, m_backgroundColor);
        }

        void fadeOut(double t)
        {
            const double r = Scene::Center().length();
            Circle(Scene::Center(), r).drawPie(0_deg, t * 360_deg, m_backgroundColor);
        }
    };

    class Stencil
    {
    private:

        Texture m_texture;

        RenderTexture m_screenTexture;

        ColorF m_backgroundColor = ColorF(0.8, 0.4, 0.0);

        void fade(double t)
        {
            m_screenTexture.clear(m_backgroundColor);
            {
                ScopedRenderTarget2D rt(m_screenTexture);
                BlendState b;
                b.writeR = b.writeG = b.writeB = false;
                b.srcAlpha = b.dstAlpha = Blend::One;
                b.opAlpha = BlendOp::RevSubtract;
                ScopedRenderStates2D blend(b);

                m_texture.scaled(EaseInSine(t) * 8.0).drawAt(Scene::Center());
            }

            m_screenTexture.draw();
        }

    public:

        Stencil()
            : m_screenTexture(Scene::Size())
        {
            m_texture = Texture(Emoji(U"🦊"));
        }

        void fadeIn(double t)
        {
            fade(t);
        }

        void fadeOut(double t)
        {
            fade(1.0 - t);
        }
    };
}

// シーンの名前
enum class State
{
    Title,
    Game
};

// ゲームデータ
struct GameData
{
    // ハイスコア
    int32 highScore = 0;

    SceneTransition::Stencil sceneTransition;
};

// シーン管理クラス
using MyApp = SceneManager<State, GameData>;


// タイトルシーン
class Title : public MyApp::Scene
{
private:

    Rect m_startButton = Rect(Arg::center = Scene::Center().movedBy(0, 0), 300, 60);
    Transition m_startTransition = Transition(0.4s, 0.2s);

    Rect m_exitButton = Rect(Arg::center = Scene::Center().movedBy(0, 100), 300, 60);
    Transition m_exitTransition = Transition(0.4s, 0.2s);

public:

    Title(const InitData& init)
        : IScene(init) {}

    void update() override
    {
        m_startTransition.update(m_startButton.mouseOver());
        m_exitTransition.update(m_exitButton.mouseOver());

        if (m_startButton.mouseOver() || m_exitButton.mouseOver())
        {
            Cursor::RequestStyle(CursorStyle::Hand);
        }

        if (m_startButton.leftClicked())
        {
            changeScene(State::Game, 3s);
        }

        if (m_exitButton.leftClicked())
        {
            System::Exit();
        }
    }

    void draw() const override
    {
        const String titleText = U"ブロックくずし";
        const Vec2 center(Scene::Center().x, 120);
        FontAsset(U"Title")(titleText).drawAt(center.movedBy(4, 6), ColorF(0.0, 0.5));
        FontAsset(U"Title")(titleText).drawAt(center);

        m_startButton.draw(ColorF(1.0, m_startTransition.value())).drawFrame(2);
        m_exitButton.draw(ColorF(1.0, m_exitTransition.value())).drawFrame(2);

        FontAsset(U"Menu")(U"はじめる").drawAt(m_startButton.center(), ColorF(0.25));
        FontAsset(U"Menu")(U"おわる").drawAt(m_exitButton.center(), ColorF(0.25));

        Rect(0, 500, Scene::Width(), Scene::Height() - 500)
            .draw(Arg::top = ColorF(0.0, 0.0), Arg::bottom = ColorF(0.0, 0.5));

        const int32 highScore = getData().highScore;
        FontAsset(U"Score")(U"High score: {}"_fmt(highScore)).drawAt(Vec2(620, 550));
    }

    void drawFadeIn(double t) const override
    {
        draw();
        getData().sceneTransition.fadeIn(t);
    }

    void drawFadeOut(double t) const override
    {
        draw();
        getData().sceneTransition.fadeOut(t);
    }
};

// ゲームシーン
class Game : public MyApp::Scene
{
private:

    // ブロックのサイズ
    static constexpr Size blockSize = Size(40, 20);

    // ボールの速さ
    static constexpr double speed = 480.0;

    // ブロックの配列
    Array<Rect> m_blocks;

    // ボールの速度
    Vec2 m_ballVelocity = Vec2(0, -speed);

    // ボール
    Circle m_ball = Circle(400, 400, 8);

    // パドル
    Rect m_paddle = Rect(Arg::center(Cursor::Pos().x, 500), 60, 10);

    // スコア
    int32 m_score = 0;

public:

    Game(const InitData& init)
        : IScene(init)
    {
        // 横 (Scene::Width() / blockSize.x) 個、縦 5 個のブロックを配列に追加する
        for (auto p : step(Size((Scene::Width() / blockSize.x), 5)))
        {
            m_blocks << Rect(p.x * blockSize.x, 60 + p.y * blockSize.y, blockSize);
        }
    }

    void update() override
    {
        // パドルを操作
        m_paddle = Rect(Arg::center(Cursor::Pos().x, 500), 60, 10);

        // ボールを移動
        m_ball.moveBy(m_ballVelocity * Scene::DeltaTime());

        // ブロックを順にチェック
        for (auto it = m_blocks.begin(); it != m_blocks.end(); ++it)
        {
            // ボールとブロックが交差していたら
            if (it->intersects(m_ball))
            {
                // ボールの向きを反転する
                (it->bottom().intersects(m_ball) || it->top().intersects(m_ball) ? m_ballVelocity.y : m_ballVelocity.x) *= -1;

                // ブロックを配列から削除(イテレータが無効になるので注意)
                m_blocks.erase(it);

                // スコアを加算
                ++m_score;

                // これ以上チェックしない  
                break;
            }
        }

        // 天井にぶつかったらはね返る
        if (m_ball.y < 0 && m_ballVelocity.y < 0)
        {
            m_ballVelocity.y *= -1;
        }

        if (m_ball.y > Scene::Height())
        {
            changeScene(State::Title, 3s);
            getData().highScore = Max(getData().highScore, m_score);
        }

        // 左右の壁にぶつかったらはね返る
        if ((m_ball.x < 0 && m_ballVelocity.x < 0) || (Scene::Width() < m_ball.x && m_ballVelocity.x > 0))
        {
            m_ballVelocity.x *= -1;
        }

        // パドルにあたったらはね返る
        if (m_ballVelocity.y > 0 && m_paddle.intersects(m_ball))
        {
            // パドルの中心からの距離に応じてはね返る向きを変える
            m_ballVelocity = Vec2((m_ball.x - m_paddle.center().x) * 10, -m_ballVelocity.y).setLength(speed);
        }
    }

    void draw() const override
    {
        FontAsset(U"Score")(m_score).drawAt(Scene::Center().x, 30);

        // すべてのブロックを描画する
        for (const auto& block : m_blocks)
        {
            block.stretched(-1).draw(HSV(block.y - 40));
        }

        // ボールを描く
        m_ball.draw();

        // パドルを描く
        m_paddle.draw();
    }

    void drawFadeIn(double t) const override
    {
        draw();
        getData().sceneTransition.fadeIn(t);
    }

    void drawFadeOut(double t) const override
    {
        draw();
        getData().sceneTransition.fadeOut(t);
    }
};

void Main()
{
    // 使用するフォントアセットを登録
    FontAsset::Register(U"Title", 120, U"example/font/AnnyantRoman/AnnyantRoman.ttf");
    FontAsset::Register(U"Menu", 30, Typeface::Regular);
    FontAsset::Register(U"Score", 36, Typeface::Bold);

    // 背景色を設定
    Scene::SetBackground(ColorF(0.2, 0.8, 0.4));

    // シーンと遷移時の色を設定
    MyApp manager;
    manager
        .add<Title>(State::Title)
        .add<Game>(State::Game)
        .setFadeColor(ColorF(1.0));

    while (System::Update())
    {
        if (!manager.update())
        {
            break;
        }
    }
}
10
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
3