【C++/OpenSiv3D】SceneManager の State にもっと情報を持たせる

OpenSiv3D のシーンマネージャー `SceneManager<State, Data>` のシーンステート `State` には、`String` や `enum class` を使うのが一般的ですが、もう少し情報を持たせたユーザ定義のクラスを使うと、各シーンの実装が少しスマートになるケースがあります。

`State` は `HashTable` のキーとして使われるため、以下の 2 つを満たす必要があります。

- キーが等しいかを調べる比較演算子が定義されていること
- `std::hash` テンプレートの特殊化が定義されていること

## サンプル
`Title`, `Story`, `Game`, `Result` の 4 種類のシーンクラスがあり、

```
Title
↓
Stage-1 Story
↓
Stage-1 Game
↓
Stage-2 Story
↓
Stage-2 Game
↓
Stage-3 Story
↓
Stage-3 Game
↓
Result
↓
(最初に戻る)
```

と進行するゲームにおいて、Stage 番号を `State` に持たせるサンプルを紹介します。

```C++
# include <Siv3D.hpp> // OpenSiv3D v0.4.2

struct GameData
{
	
};

// シーンマネージャーの State に使うクラス
struct SceneState
{
	enum class State
	{
		Title,
		Story,
		Game,
		Result,
	};

	State scene = State::Title;

	size_t stageID = 0;

	[[nodiscard]] constexpr bool hasStageID() const noexcept
	{
		return ((scene == State::Story) || (scene == State::Game));
	}

	[[nodiscard]] constexpr bool operator ==(const SceneState& other) const noexcept
	{
		return (scene == other.scene)
			&& (hasStageID() ? (stageID == other.stageID) : true);
	}

	[[nodiscard]] constexpr bool operator !=(const SceneState& other) const noexcept
	{
		return !(*this == other);
	}

	// シーンのハッシュ値
	[[nodiscard]] size_t hash() const noexcept
	{
		const size_t sceneID = static_cast<size_t>(scene) * 10000 + stageID;
		return std::hash<size_t>{}(sceneID);
	}

	[[nodiscard]] constexpr static SceneState Title() noexcept
	{
		return SceneState{ State::Title };
	}

	[[nodiscard]] constexpr static SceneState Story(size_t stageID) noexcept
	{
		return SceneState{ State::Story, stageID };
	}

	[[nodiscard]] constexpr static SceneState Game(size_t stageID) noexcept
	{
		return SceneState{ State::Game, stageID };
	}

	[[nodiscard]] constexpr static SceneState Result() noexcept
	{
		return SceneState{ State::Result };
	}
};

namespace std
{
	template <>
	struct hash<SceneState>
	{
		[[nodiscard]] size_t operator ()(const SceneState& value) const noexcept
		{
			return value.hash();
		}
	};
}

using App = SceneManager<SceneState, GameData>;

struct Title : App::Scene
{
	Title(const InitData& init)
		: IScene(init)
	{

	}

	void update() override
	{
		if (MouseL.down())
		{
			changeScene(SceneState::Story(1));
		}
	}

	void draw() const override
	{
		ClearPrint();
		Print << U"Title";
	}
};

struct Story : App::Scene
{
	Story(const InitData& init)
		: IScene(init)
	{

	}

	void update() override
	{
		if (MouseL.down())
		{
			changeScene(SceneState::Game(getState().stageID));
		}
	}

	void draw() const override
	{
		ClearPrint();
		Print << U"Stage-{} Story"_fmt(getState().stageID);
	}
};

struct Game : App::Scene
{
	Game(const InitData& init)
		: IScene(init)
	{

	}

	void update() override
	{
		if (MouseL.down())
		{
			if (getState().stageID == 3)
			{
				changeScene(SceneState::Result());
			}
			else
			{
				changeScene(SceneState::Story(getState().stageID + 1));
			}
		}
	}

	void draw() const override
	{
		ClearPrint();
		Print << U"Stage-{} Game"_fmt(getState().stageID);
	}
};

struct Result : App::Scene
{
	Result(const InitData& init)
		: IScene(init)
	{

	}

	void update() override
	{
		if (MouseL.down())
		{
			changeScene(SceneState::Title());
		}
	}

	void draw() const override
	{
		ClearPrint();
		Print << U"Result";
	}
};

void Main()
{
	Scene::SetBackground(Palette::Seagreen);

	App manager;
	manager.setFadeColor(Palette::White)
		.add<Title>(SceneState::Title())
		.add<Result>(SceneState::Result());
	for (size_t i : Range(1, 3))
	{
		manager.add<Story>(SceneState::Story(i))
			.add<Game>(SceneState::Game(i));
	}

	while (System::Update())
	{
		if (!manager.update())
		{
			break;
		}
	}
}
```