Siv3DでFlappy Birdもどきをつくったよ

# はじめに
この記事は[Siv3D Advent Calendar 2022](https://qiita.com/advent-calendar/2022/siv3d) 5日目の記事です。

こんにちは。長野高専電子情報工学科4年のたのれんと申します。[長野高専 Advent Calender 2022 4日目の記事](https://qiita.com/Tanoren/items/2fcb0efeaa1d4fac527a)にて、Siv3Dで学校祭でのゲーム展示企画におけるゲームランチャーをSiv3Dで作ったお話をしました。
そのゲームランチャーを作っている途中に展示企画に向いたシンプルなゲームを作ろうと思い、新しくSiv3DでFlappyBirdを作ったのでその紹介をします。


完成したゲームはこれ↓ Siv3Dのバージョンは0.6.5です。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">作ったゲームの紹介1<br><br>工嶺祭のためにSiv3DでFlappy Birdもどきを作りました。<br>製作期間は大体2日くらいです。<br><br>お客さんがプレイするには思ったよりも難易度が高くなってしまい、「ゲーム展示」の難しさを実感しました。<br>動画は難易度をかなり下げたものですが、そちらの方がウケが良かった気がします <a href="https://t.co/VMGxWG2y4W">https://t.co/VMGxWG2y4W</a> <a href="https://t.co/zC264ND2S7">pic.twitter.com/zC264ND2S7</a></p>&mdash; たのれん (@ninjinnokakoi) <a href="https://twitter.com/ninjinnokakoi/status/1584191449813262337?ref_src=twsrc%5Etfw">October 23, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# FlappyBirdとは？
>『Flappy Bird』（フラッピーバード）とは、ベトナムの開発者であるNguyen Ha Dong(37歳)が開発したアクションゲームである。
Flappy Birdは8ビットゲームのような外見で、非常にシンプルである。画面のタップによって画面上を飛ぶ鳥の高さを調整し、土管の隙間をぶつけずに飛ばし続けるのが目的である。
―[FlappyBird | Wikipedia](https://ja.wikipedia.org/wiki/Flappy_Bird)

先ほどのツイートを見てもらうと「あぁ、このゲームね」とピンとくる方もいらっしゃるとは思いますが、つまりは前から流れてくる障害物を避け続けるゲームです。今回はこのFlappyBirdもどきを作っていきます。

# ゲームを作ろう！
まずは大まかな設計を決めます。
とりあえず必要そうなのは
* プレイヤークラス
* 障害物クラス
* ゲームクラス

あたりですかね。一つ一つ見ていきます。

## その前に
こんな風に、ゲームの各種パラメータをグローバル変数的な感じで先に宣言しました。
```C++
// ゲームの各種パラメータ
namespace Option
{
	constexpr double JumpForce = 350.0;					// ジャンプ力
	constexpr double Glavity = 98 * 5;					// 重力加速度
	constexpr Vec2 DurationRange = Vec2{ 250, 500 };	// 次のブロックまでの距離の範囲
	constexpr Vec2 HeightRange = Vec2{ -50, 50 };		// 次のブロックの高さの範囲
	constexpr Vec2 SpaceRange = Vec2{ 450, 650 };		// ブロックの上下間隔
	constexpr Vec2 BlockSize = Vec2{ 75, 75 };			// ブロックのサイズ
	constexpr Vec2 BlockVelocity = Vec2{ -600, 0 };		// ブロックの速度
	constexpr double playerX = 300;						// プレイヤーのx座標
	constexpr double GameSpeed = 3.0;					// 速度の倍率	
}
```
今回は大体2日間で作ったmainべた書きのコードなのでこの方法が取れていますが、もっといい方法がある気がします...

## プレイヤークラス
プレイヤークラスは、座標と速度、それと本体となるRectFを持たせています。スペースキーもしくはマウスのクリックをした時にジャンプするようにします。

```C++:Playerクラス
// プレイヤークラス
class Player
{
private:
	Vec2 pos;		// 座標
	Vec2 velocity;	// 速度
	RectF body;		// 本体

public:

	// コンストラクタ
	Player()
	{
		pos = Vec2{ Option::playerX, 500 };
		velocity = Vec2{ 0, 0 };
		body = RectF{ Option::playerX, 500, 70, 140 };
	}

	// bodyを返す関数
	RectF getBody(void)
	{
		return body;
	}

	// 更新する関数
	void update(void)
	{
		// スペースキーまたは左クリックでジャンプ
		if (KeySpace.down() || MouseL.down())
		{
			velocity.y = Option::JumpForce;
		}

		// 速度の計算
		velocity.y -= Option::Glavity * Scene::DeltaTime() * Option::GameSpeed;
		// 座標の計算
		pos.y -= velocity.y * Scene::DeltaTime() * Option::GameSpeed;
		// 天井であたまごっつんこするように
		if (pos.y < 0) pos.y = 0;
		body.setPos(pos);
	}

	// 描画する関数
	void draw(void) const
	{
		body(velocity.y < 0 ? TextureAsset(U"PlayerDown") : TextureAsset(U"PlayerUp")).draw();
	}
};
```
<font color=gray>2日間クオリティですいません...コンストラクタの書き方とかは目を瞑ってください
<font color=black>何もしないままだとジャンプし続けた時に画面外へ飛んで行ってしまうので、天井で頭がごっつんこするようにしています。
それと、プレイヤーのy軸の速度が上向きか下向きかによって描画する画像を変えています。今回はスプライトとかじゃなくて力技で実装したのですが、何かありそう(調べてない)

## 障害物クラス
ブロック一つ一つをBlockクラスとし、その集まりと空洞をBarクラスとして作りました。
<img width="500" alt="BlockクラスとBarクラスの概念図" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2240661/44786663-99e0-f38f-3e26-ebb67a3d32ea.png">
### ブロッククラス

ブロッククラスは、速度を持ったRectFとほぼ同じ感じです。必要なセッタとゲッタも定義しています。色はなんとなくLimegreenにしています

```C++:Blockクラス
// ブロッククラス
class Block
{
private:
	Vec2 pos;		// 座標
	Vec2 velocity;	// 速度
	RectF body;		// 本体

public:
	Block()
	{
		pos = Vec2{ 0, 0 };
		velocity = Vec2{ 0, 0 };
		body = RectF{ pos, 0, 0 };
	}

	Block(Vec2 p, Vec2 v, Vec2 size)
	{
		pos = p;
		velocity = v;
		body = RectF{ p, size };
	}

	void setPos(Vec2 p)
	{
		pos = p;
	}

	void setVelocity(Vec2 v)
	{
		velocity = v;
	}

	void setSize(Vec2 size)
	{
		body.setSize(size);
	}

	RectF getBody(void)
	{
		return body;
	}

	void update(void)
	{
		pos.x += velocity.x * Scene::DeltaTime();
		body.setPos(pos);
	}

	void draw(void) const
	{
		body.draw(Palette::Limegreen).drawFrame(5, Palette::Dimgray);
	}
};
```
### バークラス
バークラスは、ブロッククラスの配列、座標と、通り抜けられる穴を発生させる高さとその大きさをメンバに持ちます。
穴の高さの位置から穴の大きさ分だけ空けて、それより上/下に画面外に出るまでブロックを敷き詰めています。

```C++:Barクラス
// 棒クラス
class Bar
{
private:
	Array<Block> blocks;	//ブロックたち
	double pos;				//座標
	double height;			//穴の高さ
	double space;			//穴の大きさ

public:
	Bar(double h, double s)
	{
		pos = Scene::Width();
		height = h;	//穴の高さ
		space = s;	//穴の大きさ
		int i = 0;
		while (1)	//上方向にブロック配置
		{
			double blockPos = h - s / 2 - (Option::BlockSize.x + 10) * i;
			blocks << Block{ Vec2{pos, blockPos}, Option::BlockVelocity, Option::BlockSize };
			i++;
			if (blockPos < 0) break;
		}

		i = 0;

		while (1)	//下方向にブロック配置
		{
			double blockPos = h + s / 2 + (Option::BlockSize.x + 10) * i;
			blocks << Block{ Vec2{pos, blockPos}, Option::BlockVelocity, Option::BlockSize };
			i++;
			if (blockPos > Scene::Height()) break;
		}
	}

	double getPos(void)
	{
		return pos;
	}

	double getHeight(void)
	{
		return height;
	}

	Array<Block> getBlocks(void)
	{
		return blocks;
	}

	void update(void)
	{
		for (auto i : step(blocks.size())) blocks[i].update();
		pos += Option::BlockVelocity.x * Scene::DeltaTime();
	}

	void draw(void) const
	{
		for (auto i : step(blocks.size())) blocks[i].draw();
	}
};
```

## ゲームクラス
ゲームクラスは、最低限メンバにプレイヤークラス、障害物クラス、次の障害物までの距離、死亡を表すフラグを宣言します。

```C++:
// ゲームシーン
class Game
{
private:
	Player player;		//プレイヤー
	Array<Bar> bar;		//障害物
	double next;		//次の障害物までの距離
	bool dead = false;	//死亡フラグ

public:
    void update();
    void draw();
};
```

次にアップデート関数を書きます。
一番後ろのバーが、次の障害物までの距離よりも進んだら新しくバーを生成させます。
プレイヤーとブロック一つ一つの当たり判定を行い、当たっていたら死亡フラグを立たせます。

```C++:update関数
void update() override
{
	// 次の障害物までの距離よりバーが進んだら新しく障害物を生成
	if (Scene::Width() - bar[bar.size() - 1].getPos() > next)
	{
        //次の障害物までの距離を決める
		next = Random(Option::DurationRange.x, Option::DurationRange.y);
		bar << Bar{ Clamp(bar[bar.size() - 1].getHeight() + Random(Option::HeightRange.x, Option::HeightRange.y),
                0.0, (double)Scene::Height()), Random(Option::SpaceRange.x, Option::SpaceRange.y) };
	}

	if (!dead)
	{
		player.update();
		for (auto i : step(bar.size()))
		{
			bar[i].update();
			// 障害物の棒のブロックの配列を取得
			for (auto j : step(bar[i].getBlocks().size()))
			{
				// ブロックにぶつかるか地面に落ちたら死亡フラグを立てる
				if (bar[i].getBlocks()[j].getBody().intersects(player.getBody()) || player.getBody().pos.y > Scene::Height() - 100)
				{
					dead = true;
				}
			}
		}
		// バーが画面から出たら消去
		if (bar[0].getPos() < -80) bar.pop_front();
	}
}
```

これで大まかな骨組みは出来たのではないでしょうか？

## 完成
あとはdrawしてあげたりタイトル画面を作って完成です。
ちゃんとゲームとして完成させるにはコア部分だけでなく、タイトル画面やGUI諸々を考えないといけないのですが

一応コード全文を載せます。長いです(596行)。コインが出てきたり、コインを取った時にエフェクトが発生したり、スコアランキングがあったりします。作り切りなのでコメントやら何やらが統一されていませんが悪しからず...あと画像は適時用意してください。
<details><summary>コード全文</summary>

```C++:main.cpp
# include <Siv3D.hpp> // OpenSiv3D v0.6.5


// ゲームの各種パラメータ
namespace Option
{
	constexpr double JumpForce = 350.0;					// ジャンプ力
	constexpr double Glavity = 98 * 5;					// 重力加速度
	constexpr Vec2 DurationRange = Vec2{ 250, 500 };	// 次のブロックまでの距離の範囲
	constexpr Vec2 HeightRange = Vec2{ -50, 50 };		// 次のブロックの高さの範囲
	constexpr Vec2 SpaceRange = Vec2{ 450, 650 };		// ブロックの上下間隔
	constexpr Vec2 BlockSize = Vec2{ 75, 75 };			// ブロックのサイズ
	constexpr Vec2 BlockVelocity = Vec2{ -600, 0 };		// ブロックの速度
	constexpr double playerX = 300;						// プレイヤーのx座標
	constexpr double GameSpeed = 3.0;					// 速度の倍率	
}

// プレイヤークラス
class Player
{
private:
	Vec2 pos;		// 座標
	Vec2 velocity;	// 速度
	RectF body;		// 本体

public:

	// コンストラクタ
	Player()
	{
		pos = Vec2{ Option::playerX, 500 };
		velocity = Vec2{ 0, 0 };
		body = RectF{ Option::playerX, 500, 70, 140 };
	}

	// bodyを返す関数
	RectF getBody(void)
	{
		return body;
	}

	// 更新する関数
	void update(void)
	{
		// スペースキーまたは左クリックでジャンプ
		if (KeySpace.down() || MouseL.down())
		{
			velocity.y = Option::JumpForce;
		}

		// 速度の計算
		velocity.y -= Option::Glavity * Scene::DeltaTime() * Option::GameSpeed;
		// 座標の計算
		pos.y -= velocity.y * Scene::DeltaTime() * Option::GameSpeed;
		// 天井であたまごっつんこするように
		if (pos.y < 0) pos.y = 0;
		body.setPos(pos);
	}

	// 描画する関数
	void draw(void) const
	{
		body(velocity.y < 0 ? TextureAsset(U"PlayerDown") : TextureAsset(U"PlayerUp")).draw();
	}
};

// ブロッククラス
class Block
{
private:
	Vec2 pos;		// 座標
	Vec2 velocity;	// 速度
	RectF body;		// 本体

public:
	Block()
	{
		pos = Vec2{ 0, 0 };
		velocity = Vec2{ 0, 0 };
		body = RectF{ pos, 0, 0 };
	}

	Block(Vec2 p, Vec2 v, Vec2 size)
	{
		pos = p;
		velocity = v;
		body = RectF{ p, size };
	}

	void setPos(Vec2 p)
	{
		pos = p;
	}

	void setVelocity(Vec2 v)
	{
		velocity = v;
	}

	void setSize(Vec2 size)
	{
		body.setSize(size);
	}

	RectF getBody(void)
	{
		return body;
	}

	void update(void)
	{
		pos.x += velocity.x * Scene::DeltaTime();
		body.setPos(pos);
	}

	void draw(void) const
	{
		body.draw(Palette::Limegreen).drawFrame(5, Palette::Dimgray);
	}
};

// 棒クラス
class Bar
{
private:
	Array<Block> blocks;	//ブロックたち
	double pos;				//座標
	double height;			//穴の高さ
	double space;			//穴の大きさ

public:
	Bar(double h, double s)
	{
		pos = Scene::Width();
		height = h;	//穴の高さ
		space = s;	//穴の大きさ
		int i = 0;
		while (1)	//上方向にブロック配置
		{
			double blockPos = h - s / 2 - (Option::BlockSize.x + 10) * i;
			blocks << Block{ Vec2{pos, blockPos}, Option::BlockVelocity, Option::BlockSize };
			i++;
			if (blockPos < 0) break;
		}

		i = 0;

		while (1)	//下方向にブロック配置
		{
			double blockPos = h + s / 2 + (Option::BlockSize.x + 10) * i;
			blocks << Block{ Vec2{pos, blockPos}, Option::BlockVelocity, Option::BlockSize };
			i++;
			if (blockPos > Scene::Height()) break;
		}
	}

	double getPos(void)
	{
		return pos;
	}

	double getHeight(void)
	{
		return height;
	}

	Array<Block> getBlocks(void)
	{
		return blocks;
	}

	void update(void)
	{
		for (auto i : step(blocks.size())) blocks[i].update();
		pos += Option::BlockVelocity.x * Scene::DeltaTime();
	}

	void draw(void) const
	{
		for (auto i : step(blocks.size())) blocks[i].draw();
	}
};

// コインを取った時のエフェクト
struct CoinEffect : IEffect
{
	static constexpr Vec2 Gravity{ 0, 160 };

	struct Bubble
	{
		Vec2 start;
		Vec2 velocity;
		ColorF color;
	};

	Array<Bubble> m_bubbles;

	CoinEffect(const Vec2& pos, double baseHue)
	{
		for (int32 i = 0; i < 6; ++i)
		{
			const Vec2 velocity = RandomVec2(Circle{ 60 });
			Bubble bubble{
				.start = (pos + velocity),
				.velocity = velocity,
				.color = HSV{ baseHue + Random(-20.0, 20.0) },
			};
			m_bubbles << bubble;
		}
	}

	bool update(double t) override
	{
		t /= 0.4;

		for (auto& bubble : m_bubbles)
		{
			const Vec2 pos = bubble.start
				+ bubble.velocity * t + 0.5 * t * t * Gravity;

			const double angle = (pos.x * 3_deg);

			Circle(pos, (30 * (1.0 - t)))
				.draw(HSV(bubble.color, 1.0 - t));
		}

		return (t < 1.0);
	}
};

struct GameData
{
	Array<int32> scores;
};

using App = SceneManager<String, GameData>;

// タイトルシーン
class Title : public App::Scene
{
private:
	int now;
	String start, ranking, quit;
	Vec2 sPos, rPos, qPos;
	bool Rank;
	RectF sRect, rRect, qRect;
	RectF RankBoard, Back;

public:

	Title(const InitData& init)
		: IScene{ init }
	{
		Rank = false;
		now = 0;
		start = U"START";
		ranking = U"RANKING";
		quit = U"QUIT";
		sPos = Vec2{ Scene::Width() / 2, 700 };
		rPos = Vec2{ Scene::Width() / 2, 800 };
		qPos = Vec2{ Scene::Width() / 2, 900 };
		sRect = RectF{ Arg::center(sPos), 500, 80 };
		rRect = RectF{ Arg::center(rPos), 500, 80 };
		qRect = RectF{ Arg::center(qPos), 500, 80 };
		RankBoard = RectF{ Arg::center(Scene::Width() / 2, Scene::Height() / 2 - 75), 800, 800 };
		Back = RectF{ Arg::center(Scene::Width() / 2, 900), 300, 75 };
		getData().scores.clear();

		{
			TextReader reader{ U"Score.txt" };
			if (!reader)
			{
				reader.close();
				TextWriter writer{ U"Score.txt" };
				writer.close();
			}
		}

		TextReader reader{ U"Score.txt" };
		String line;
		while (reader.readLine(line))
		{
			getData().scores << Parse<int32>(line);
		}

		if (getData().scores.size() < 10)
		{
			int num = 10 - getData().scores.size();
			TextWriter writer{ U"Score.txt", OpenMode::Append };
			for (auto i : step(num))
			{
				writer.writeln(U"0");
				getData().scores << 0;
			}
		}

		getData().scores.sort_by([](int a, int b) { return a > b; });
		TextWriter writer{ U"Score.txt" };
		for (auto i : step(getData().scores.size()))
		{
			writer.writeln(Format(getData().scores[i]));
		}
	}

	void update() override
	{
		if (KeyUp.down())
		{
			now--;
			if (now < 0) now += 3;
		}
		if (KeyDown.down())
		{
			now++;
		}
		now %= 3;

		if (sRect.mouseOver()) now = 0;
		if (rRect.mouseOver()) now = 1;
		if (qRect.mouseOver()) now = 2;

		if (MouseL.down() || KeyEnter.down())
		{
			if (!Rank)
			{
				switch (now)
				{
				case 0:
					changeScene(U"Game", 0.3s);
					break;
				case 1:
					Rank = true;
					break;
				case 2:
					System::Exit();
					break;
				default:	//never happend
					break;
				}
			}
		}

		if (Rank && Back.leftClicked()) Rank = false;
	}

	void draw() const override
	{
		switch (now)
		{
		case 0:
			sRect.draw(Arg::left = ColorF(1.0, 0.15), Arg::right = ColorF{ 1.0, 0.75 });
			break;
		case 1:
			rRect.draw(Arg::left = ColorF(1.0, 0.15), Arg::right = ColorF{ 1.0, 0.75 });
			break;
		case 2:
			qRect.draw(Arg::left = ColorF(1.0, 0.15), Arg::right = ColorF{ 1.0, 0.75 });
			break;
		default:	//never happend
			break;
		}
		TextureAsset(U"TitleLogo").scaled(1.5).drawAt(Scene::Width() / 2, 450);
		FontAsset(U"Title")(start).drawAt(sPos.movedBy(4, 4), ColorF{ 0, 0.5 });
		FontAsset(U"Title")(start).drawAt(sPos, Palette::Mediumaquamarine);
		FontAsset(U"Title")(ranking).drawAt(rPos.movedBy(4, 4), ColorF{ 0, 0.5 });
		FontAsset(U"Title")(ranking).drawAt(rPos, Palette::Lightyellow);
		FontAsset(U"Title")(quit).drawAt(qPos.movedBy(4, 4), ColorF{ 0, 0.5 });
		FontAsset(U"Title")(quit).drawAt(qPos, Palette::Palevioletred);

		if (Rank)
		{
			RectF{ 0, 0, Scene::Size() }.draw(ColorF(0, 0.5));
			RankBoard.drawFrame(10, Palette::Dimgray).draw(Palette::Azure);
			Back.drawFrame(15, Palette::Darkgreen).draw(Palette::Seagreen);

			FontAsset(U"GameOver")(U"RANKING").drawAt(Scene::Width() / 2, 55, Palette::Dimgray);

			for (auto i : step(10))
			{
				FontAsset(U"Rank")(Format(i + 1) + U".").drawAt(654, 154 + i * 67, ColorF(0, 0.5));
				FontAsset(U"Rank")(Format(i + 1) + U".").drawAt(650, 150 + i * 67, Palette::Aquamarine);
				FontAsset(U"Rank")(Format(getData().scores[i])).drawAt(Scene::Width() / 2 + 4, 154 + i * 67, ColorF(0, 0.5));
				FontAsset(U"Rank")(Format(getData().scores[i])).drawAt(Scene::Width() / 2, 150 + i * 67, Palette::Khaki);
			}

			FontAsset(U"Title")(U"BACK").drawAt(Back.center(), Palette::White);
		}
	}
};

// ゲームシーン
class Game : public App::Scene
{
private:
	Player player;		//プレイヤー
	Array<Bar> bar;		//障害物
	Array<RectF> coins;	//コイン
	double next;		//次の障害物までの距離
	bool dead = false;	//死亡フラグ
	bool Pause = true;	//一時停止フラグ
	bool coin = false;	//コインを取った時のフラグ
	double score;		//スコア
	int now;			//今選択されている選択肢
	Effect effect;		//コインのエフェクト
	bool initF = false;	//スコアを書き込むためのフラグ
	RectF TITLE, RETRY;

public:

	Game(const InitData& init)
		: IScene{ init }
	{
		player = Player();
		bar.clear();
		bar << Bar{ 300, 500 };
		coins.clear();
		next = Random(Option::DurationRange.x, Option::DurationRange.y);
		dead = false;
		Pause = true;
		coin = false;
		score = 0;
		now = 1;
		initF = false;

		TITLE = RectF{ Arg::center(Vec2{650, 800}), 500, 100 };
		RETRY = RectF{ Arg::center(Vec2{1330, 800}), 500, 100 };
	}

	void update() override
	{
		// pキーが押されたら一時停止
		if (KeyP.down()) Pause ^= 1;
		// 一時停止中にスペースまたはマウスをクリックで一時停止解除
		if (Pause && (KeySpace.down() || MouseL.down())) Pause = false;
		// 新しく障害物を生成
		if (Scene::Width() - bar[bar.size() - 1].getPos() > next)
		{
			next = Random(Option::DurationRange.x, Option::DurationRange.y);
			bar << Bar{ Clamp(bar[bar.size() - 1].getHeight() + Random(Option::HeightRange.x, Option::HeightRange.y), 0.0, (double)Scene::Height()), Random(Option::SpaceRange.x, Option::SpaceRange.y) };
			if (Random(0, 10) > 7)
			{
				coins << RectF{ bar[bar.size() - 1].getPos() + Random(0, 250), bar[bar.size() - 1].getHeight() + Random(-50, 50), 50, 50 };
			}
		}

		if (!Pause && !dead)
		{
			int index = 0;

			// コインの更新
			for (auto i : step(coins.size()))
			{
				coins[i].setPos(coins[i].pos.x + Option::BlockVelocity.x * Scene::DeltaTime(), coins[i].pos.y);
				if (coins[i].pos.x < -50) coins.pop_front();
				if (coins[i].intersects(player.getBody()))
				{
					score += 50;
					index = i;
					coin = true;
					effect.add<CoinEffect>(coins[i].center(), 60);
				}
			}

			if (coin)
			{
				coins.remove(coins[index]);
				coin = false;
			}

			score += Scene::DeltaTime() * 10;
			player.update();
			for (auto i : step(bar.size()))
			{
				bar[i].update();

				// 障害物の棒のブロックの配列を取得
				for (auto j : step(bar[i].getBlocks().size()))
				{
					// 障害物の棒のブロック一つ一つを取得
					// ぶつかったら死亡フラグを立てる
					if (bar[i].getBlocks()[j].getBody().intersects(player.getBody()) || player.getBody().pos.y > Scene::Height() - 100)
					{
						dead = true;
					}
				}
			}

			// バーが画面から出たら消去
			if (bar[0].getPos() < -80) bar.pop_front();
		}

		if (KeyRight.down() || KeyLeft.down()) now ^= 1;

		if (dead)
		{
			if (TITLE.mouseOver()) now = 0;
			if (RETRY.mouseOver()) now = 1;
			if (!initF) //データ更新
			{
				initF = true;
				getData().scores << (int32)score;
				getData().scores.sort_by([](int a, int b) { return a > b; });
				TextWriter writer{ U"Score.txt" };
				for (auto i : step(getData().scores.size()))
				{
					writer.writeln(Format(getData().scores[i]));
				}
			}
			if (KeyEnter.down() || RectF{ Arg::center(Vec2{now == 0 ? 650 : 1330, 800}), 500, 100 }.leftClicked())
			{
				if (now) changeScene(U"Game", 0.0s);
				else changeScene(U"Title", 0.3s);
			}
		}
	}

	void draw() const override
	{
		player.draw();
		for (auto i : step(bar.size())) bar[i].draw();
		FontAsset(U"Explain")(U"SCORE:").drawAt(150, 50);
		FontAsset(U"Explain")(Format((int)score)).drawAt(350, 50);

		for (auto i : step(coins.size())) coins[i](TextureAsset(U"Coin")).draw();

		effect.update();

		if (Pause)
		{
			RectF{ Vec2{0, 0}, Scene::Size() }.draw(ColorF(0, 0.25));
			FontAsset(U"Explain")(U"スペースキーもしくはマウスクリックで開始").drawAt(Scene::Center());
		}

		if (dead)
		{
			RectF{ Vec2{0, 0}, Scene::Size() }.draw(ColorF(0, 0.25));
			FontAsset(U"GameOver")(U"GAME OVER!").drawAt(Vec2{ Scene::Width() / 2, 150 }.movedBy(4, 4), ColorF(0, 0.5));
			FontAsset(U"GameOver")(U"GAME OVER!").drawAt(Scene::Width() / 2, 150, Palette::Lightcyan);

			FontAsset(U"GameOver")(U"SCORE:").drawAt(754, 404, ColorF(0, 0.5));
			FontAsset(U"GameOver")(U"SCORE:").drawAt(750, 400, Palette::Yellow);
			FontAsset(U"GameOver")(Format((int)score)).drawAt(1204, 404, ColorF(0, 0.5));
			FontAsset(U"GameOver")(Format((int)score)).drawAt(1200, 400, Palette::Mediumspringgreen);

			if (score > getData().scores[0])
			{
				FontAsset(U"GameOver")(U"HIGH SCORE!").drawAt(Scene::Center() + Vec2{ 0, 100 }.movedBy(4, 4), ColorF(0, 0.5));
				FontAsset(U"GameOver")(U"HIGH SCORE!").drawAt(Scene::Center() + Vec2{ 0, 100 }, Periodic::Square0_1(0.25s) ? Palette::Red : Palette::Yellow);
			}
			else if (score > getData().scores[9])
			{
				FontAsset(U"GameOver")(U"RANK IN!").drawAt(Scene::Center() + Vec2{ 0, 100 }.movedBy(4, 4), ColorF(0, 0.5));
				FontAsset(U"GameOver")(U"RANK IN!").drawAt(Scene::Center() + Vec2{ 0, 100 }, Palette::Yellow);
			}

			RectF{ Arg::center(Vec2{now == 0 ? 650 : 1330, 800}), 500, 100 }.draw(ColorF(1, 0.5));
			FontAsset(U"GameOver")(U"TITLE").drawAt(Vec2{ 650, 800 }.movedBy(4, 4), ColorF(0, 0.5));
			FontAsset(U"GameOver")(U"TITLE").drawAt(Vec2{ 650, 800 }, Palette::Aquamarine);
			FontAsset(U"GameOver")(U"RETRY").drawAt(Vec2{ 1330, 800 }.movedBy(4, 4), ColorF(0, 0.5));
			FontAsset(U"GameOver")(U"RETRY").drawAt(Vec2{ 1330, 800 }, Palette::Deeppink);
		}
	}
};

void Main()
{
	//Window::SetFullscreen(true);
	Window::Resize(1920, 980);

	TextureAsset::Register(U"Background", U"assets/background.png", TextureDesc::Mipped);
	TextureAsset::Register(U"PlayerUp", U"assets/siv3d-kun_up.png", TextureDesc::Mipped);
	TextureAsset::Register(U"PlayerDown", U"assets/siv3d-kun_down.png", TextureDesc::Mipped);
	TextureAsset::Register(U"TitleLogo", U"assets/titleLogo.png", TextureDesc::Mipped);
	TextureAsset::Register(U"Coin", U"assets/coin.png", TextureDesc::Mipped);

	FontAsset::Register(U"Title", 80, Typeface::Heavy);
	FontAsset::Register(U"Explain", 60, Typeface::Heavy);
	FontAsset::Register(U"GameOver", 100, Typeface::Bold);
	FontAsset::Register(U"Rank", 60, Typeface::Medium);

	RectF bg = RectF{ 0, 0, Scene::Size() };

	App manager;
	manager.add<Title>(U"Title");
	manager.add<Game>(U"Game");

	// フェードイン・フェードアウト時の画面の色
	manager.setFadeColor(ColorF{ 0.8, 0.9, 1.0 });

	while (System::Update())
	{
		bg(TextureAsset(U"Background")).draw();
		if (!manager.update()) break;
	}
}

```
</details>

# おわりに
学校祭のゲーム展示企画でFlappyBirdを作りました。ゲーム自体のシンプルさがよかったのか、そこそこ遊んでくれたみたいなので良かったです。
Siv3Dで簡単なゲームを作ってみると意外と難しかったりちゃんと考えないと先に進めないことがあるので、いい感じのトレーニングになりそう。
そして何より無限に遊べるのがいいですね。

来年は時間があったらShaderの勉強してその記事を書いてみたいです。

おしまい