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

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

## 時計
時計のように回転しながら背景色に切り替わります。
![SceneTransition1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30255/b2903366-3eff-5eb5-5607-5bdd91b42bfd.gif)

```C++
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30255/ea183708-d9f0-60e8-a1dc-361f05cf0a9b.gif)

```C++
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 override` と `drawFadeOut(double t) const override` を実装し、その中で `draw()` を呼んだあとに `sceneTransition.fadeIn(t)` または `sceneTransition.fadeOut(t)` を呼びます。

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

## 使い方サンプル
[シーン遷移のサンプル](https://siv3d.github.io/ja-jp/sample/game-template/) に、今回のエフェクトを組み込みます。

```C++
# 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;
		}
	}
}
```