🍰

Siv3D | 落ちてくる食べ物を集めるゲームを作る Day 2

2021/03/22に公開

このコースでは、初歩的な C++ の文法と OpenSiv3D の機能を使って、落ちてくる食べ物を集めるシンプルなゲームを作ります。

9. 食べ物を動かす

食べ物を落下させましょう。

食べ物の移動量はプレイヤーの移動と同じように、スピードとフレーム時間をもとに計算します。

さらに、プログラムの見通しをよくするために、メインループの中を 3 つのセクション「プレイヤーの移動」「食べ物の移動」「描画」に分け、それぞれ { } のスコープで囲みます。これによりサンプルにある const double move のような局所的な変数が他のセクションで使われることを防ぎ、変数のスコープが限定されている良い状態になります。(1 つの変数があちこちで使えてしまうと、プログラムの変更やメンテナンスが大変になります)

{
	// セクション A
	{
		int32 a = 10; // この変数が使えるのは現在のスコープ { } 内のみ
	}

	// セクション B
	{
		// ここで a は使えない
	}

	// セクション C
	{
		int32 a = 30; // ここで別の変数 a を作れる
	}
}
# include <Siv3D.hpp> // OpenSiv3D v0.4.3

// 食べ物クラス
struct Food
{
	Vec2 pos;
};

void Main()
{
	// プレイヤーのテクスチャ
	const Texture playerTexture{ Emoji{ U"😋" } };

	// 食べ物のテクスチャ
	const Texture foodTexture{ Emoji{ U"🍰" } };

	// プレイヤーが毎秒何ピクセルの速さで移動するか
	double playerSpeed = 300.0;

	// 食べ物が毎秒何ピクセルの速さで落下するか
	double foodSpeed = 200.0;

	// プレイヤーの位置
	Vec2 playerPos{ 400, 500 };

	// 食べ物
	Array<Food> foods;
	foods << Food{ .pos = Vec2{ 300, 100 } };
	foods << Food{ .pos = Vec2{ 500, 200 } };
	foods << Food{ .pos = Vec2{ 700, 300 } };

	while (System::Update())
	{
		// プレイヤーの移動
		{
			if (KeyLeft.pressed()) // 左矢印キーが押されていたら
			{
				playerPos.x -= (playerSpeed * Scene::DeltaTime());
			}
			else if (KeyRight.pressed()) // 右矢印キーが押されていたら
			{
				playerPos.x += (playerSpeed * Scene::DeltaTime());
			}

			// playerPos.x を 50 以上、750 以下に収める
			playerPos.x = Clamp(playerPos.x, 50.0, 750.0);
		}

		// 食べ物の移動
		{
			const double move = (foodSpeed * Scene::DeltaTime());

			for (auto& food : foods)
			{
				food.pos.y += move;
			}
		}

		// 描画
		{
			// 空を描く(グラデーション)
			Rect{ 0, 0, 800, 520 }
				.draw(Arg::top = ColorF{ 0.1, 0.3, 0.6 }, Arg::bottom = ColorF{ 0.3, 0.7, 1.0 });

			// 地面を描く
			Rect{ 0, 520, 800, 80 }.draw(ColorF{ 0.2, 0.6, 0.3 });

			// プレイヤーを描く
			playerTexture.drawAt(playerPos);

			for (const auto& food : foods)
			{
				// 食べ物を描く(サイズは 0.5 倍)
				foodTexture.scaled(0.5).drawAt(food.pos);
			}
		}
	}
}

10. 地面に落ちた食べ物を消す

この先、食べ物を増やしていくとすると、食べ物をいつまでも落下させていると処理のコストがどんどん増えてしまいます。地面に落ちた食べ物は配列から削除し、移動や描画の処理が発生しないようにします。

Array のメンバ変数 .remove_if(pred) は、条件 pred を満たす要素を配列から削除します。食べ物が地面に落ちたかを判定する関数 CheckFood() を作り、これを remove_if() に渡しましょう。

また、ゲームの変数の状態がわかりやすいように、「デバッグ表示」セクションで食べ物の個数を可視化します。

# include <Siv3D.hpp> // OpenSiv3D v0.4.3

// 食べ物クラス
struct Food
{
	Vec2 pos;
};

// 食べ物を消す条件を記述した関数
bool CheckFood(const Food& food)
{
	// 食べ物の Y 座標が 600 より大きいか
	return (600 < food.pos.y);
}

void Main()
{
	// プレイヤーのテクスチャ
	const Texture playerTexture{ Emoji{ U"😋" } };

	// 食べ物のテクスチャ
	const Texture foodTexture{ Emoji{ U"🍰" } };

	// プレイヤーが毎秒何ピクセルの速さで移動するか
	double playerSpeed = 300.0;

	// 食べ物が毎秒何ピクセルの速さで落下するか
	double foodSpeed = 200.0;

	// プレイヤーの位置
	Vec2 playerPos{ 400, 500 };

	// 食べ物
	Array<Food> foods;
	foods << Food{ .pos = Vec2{ 300, 100 } };
	foods << Food{ .pos = Vec2{ 500, 200 } };
	foods << Food{ .pos = Vec2{ 700, 300 } };

	while (System::Update())
	{
		// デバッグ表示
		{
			ClearPrint();
			Print << U"食べ物の個数: " << foods.size();
		}

		// プレイヤーの移動
		{
			if (KeyLeft.pressed()) // 左矢印キーが押されていたら
			{
				playerPos.x -= (playerSpeed * Scene::DeltaTime());
			}
			else if (KeyRight.pressed()) // 右矢印キーが押されていたら
			{
				playerPos.x += (playerSpeed * Scene::DeltaTime());
			}

			// playerPos.x を 50 以上、750 以下に収める
			playerPos.x = Clamp(playerPos.x, 50.0, 750.0);
		}

		// 食べ物の移動
		{
			const double move = (foodSpeed * Scene::DeltaTime());

			for (auto& food : foods)
			{
				food.pos.y += move;
			}
		}

		// 地面に落ちた食べ物の消去
		{
			// 条件を満たす要素を配列から削除
			foods.remove_if(CheckFood);
		}

		// 描画
		{
			// 空を描く(グラデーション)
			Rect{ 0, 0, 800, 520 }
				.draw(Arg::top = ColorF{ 0.1, 0.3, 0.6 }, Arg::bottom = ColorF{ 0.3, 0.7, 1.0 });

			// 地面を描く
			Rect{ 0, 520, 800, 80 }.draw(ColorF{ 0.2, 0.6, 0.3 });

			// プレイヤーを描く
			playerTexture.drawAt(playerPos);

			for (const auto& food : foods)
			{
				// 食べ物を描く(サイズは 0.5 倍)
				foodTexture.scaled(0.5).drawAt(food.pos);
			}
		}
	}
}

11. 地面に落ちた食べ物を消す (ラムダ式を使って改良)

「ラムダ式」という C++ の文法を使うと、CheckFood() 関数を remove_if() の中にラムダ式で記述でき、プログラムが短くなります。

# include <Siv3D.hpp> // OpenSiv3D v0.4.3

// 食べ物クラス
struct Food
{
	Vec2 pos;
};

void Main()
{
	// プレイヤーのテクスチャ
	const Texture playerTexture{ Emoji{ U"😋" } };

	// 食べ物のテクスチャ
	const Texture foodTexture{ Emoji{ U"🍰" } };

	// プレイヤーが毎秒何ピクセルの速さで移動するか
	double playerSpeed = 300.0;

	// 食べ物が毎秒何ピクセルの速さで落下するか
	double foodSpeed = 200.0;

	// プレイヤーの位置
	Vec2 playerPos{ 400, 500 };

	// 食べ物
	Array<Food> foods;
	foods << Food{ .pos = Vec2{ 300, 100 } };
	foods << Food{ .pos = Vec2{ 500, 200 } };
	foods << Food{ .pos = Vec2{ 700, 300 } };

	while (System::Update())
	{
		// デバッグ表示
		{
			ClearPrint();
			Print << U"食べ物の個数: " << foods.size();
		}

		// プレイヤーの移動
		{
			if (KeyLeft.pressed()) // 左矢印キーが押されていたら
			{
				playerPos.x -= (playerSpeed * Scene::DeltaTime());
			}
			else if (KeyRight.pressed()) // 右矢印キーが押されていたら
			{
				playerPos.x += (playerSpeed * Scene::DeltaTime());
			}

			// playerPos.x を 50 以上、750 以下に収める
			playerPos.x = Clamp(playerPos.x, 50.0, 750.0);
		}

		// 食べ物の移動
		{
			const double move = (foodSpeed * Scene::DeltaTime());

			for (auto& food : foods)
			{
				food.pos.y += move;
			}
		}

		// 地面に落ちた食べ物の消去
		{
			// 条件を満たす要素を配列から削除(ラムダ式で記述)
			foods.remove_if([](const Food& food){ return (600 < food.pos.y); });
		}

		// 描画
		{
			// 空を描く(グラデーション)
			Rect{ 0, 0, 800, 520 }
				.draw(Arg::top = ColorF{ 0.1, 0.3, 0.6 }, Arg::bottom = ColorF{ 0.3, 0.7, 1.0 });

			// 地面を描く
			Rect{ 0, 520, 800, 80 }.draw(ColorF{ 0.2, 0.6, 0.3 });

			// プレイヤーを描く
			playerTexture.drawAt(playerPos);

			for (const auto& food : foods)
			{
				// 食べ物を描く(サイズは 0.5 倍)
				foodTexture.scaled(0.5).drawAt(food.pos);
			}
		}
	}
}

12. 食べ物を出現させる

一定時間が経過したときに食べ物を出現させましょう。

何秒ごとに食べ物が出現するかを表す変数 foodSpawnTime, 前回の食べ物の出現から何秒経過したかを表す変数 timeAccumulator を追加し、「食べ物の出現」セクションで一定時間ごとにランダムな場所に食べ物を発生させます。Random(min, max)min 以上 max 以下のランダムな数を返す関数です。

# include <Siv3D.hpp> // OpenSiv3D v0.4.3

// 食べ物クラス
struct Food
{
	Vec2 pos;
};

void Main()
{
	// プレイヤーのテクスチャ
	const Texture playerTexture{ Emoji{ U"😋" } };

	// 食べ物のテクスチャ
	const Texture foodTexture{ Emoji{ U"🍰" } };

	// プレイヤーが毎秒何ピクセルの速さで移動するか
	double playerSpeed = 300.0;

	// 食べ物が毎秒何ピクセルの速さで落下するか
	double foodSpeed = 200.0;

	// 何秒ごとに食べ物が出現するか
	double foodSpawnTime = 1.0;

	// 前回の食べ物の出現から何秒経過したか
	double timeAccumulator = 0.0;

	// プレイヤーの位置
	Vec2 playerPos{ 400, 500 };

	// 食べ物
	Array<Food> foods;
	foods << Food{ .pos = Vec2{ 300, 100 } };
	foods << Food{ .pos = Vec2{ 500, 200 } };
	foods << Food{ .pos = Vec2{ 700, 300 } };

	while (System::Update())
	{
		// デバッグ表示
		{
			ClearPrint();
			Print << U"食べ物の個数: " << foods.size();
			Print << U"timeAccumulator: " << timeAccumulator;
		}

		// プレイヤーの移動
		{
			if (KeyLeft.pressed()) // 左矢印キーが押されていたら
			{
				playerPos.x -= (playerSpeed * Scene::DeltaTime());
			}
			else if (KeyRight.pressed()) // 右矢印キーが押されていたら
			{
				playerPos.x += (playerSpeed * Scene::DeltaTime());
			}

			// playerPos.x を 50 以上、750 以下に収める
			playerPos.x = Clamp(playerPos.x, 50.0, 750.0);
		}

		// 食べ物の出現
		{
			// 経過時間の蓄積
			timeAccumulator += Scene::DeltaTime();

			// 食べ物の出現時間よりも長く経過していた場合
			while (foodSpawnTime <= timeAccumulator)
			{
				// 食べ物をランダムな場所に出現させる
				foods << Food{ .pos = Vec2{ Random(50.0, 750.0), -50 } };

				// 経過時間を減らす
				timeAccumulator -= foodSpawnTime;
			}
		}

		// 食べ物の移動
		{
			const double move = (foodSpeed * Scene::DeltaTime());

			for (auto& food : foods)
			{
				food.pos.y += move;
			}
		}

		// 地面に落ちた食べ物の消去
		{
			// 条件を満たす要素を配列から削除(ラムダ式で記述)
			foods.remove_if([](const Food& food) { return (600 < food.pos.y); });
		}

		// 描画
		{
			// 空を描く(グラデーション)
			Rect{ 0, 0, 800, 520 }
				.draw(Arg::top = ColorF{ 0.1, 0.3, 0.6 }, Arg::bottom = ColorF{ 0.3, 0.7, 1.0 });

			// 地面を描く
			Rect{ 0, 520, 800, 80 }.draw(ColorF{ 0.2, 0.6, 0.3 });

			// プレイヤーを描く
			playerTexture.drawAt(playerPos);

			for (const auto& food : foods)
			{
				// 食べ物を描く(サイズは 0.5 倍)
				foodTexture.scaled(0.5).drawAt(food.pos);
			}
		}
	}
}

13. プレイヤーが食べ物をゲットする

プレイヤーが食べ物に触れたときに、その食べ物を消去するようにします。

イテレータを使うことで、要素への順番のアクセスと要素の削除を同時に処理できます。次のようなループを書きます。

for (auto it = foods.begin(); it != foods.end();)
{
	if (...) // 要素を削除する場合
	{
		it = foods.erase(it); // 要素を削除して次の要素へ
	}
	else // 要素を削除しない場合
	{
		++it; // 次の要素へ
	}
}
# include <Siv3D.hpp> // OpenSiv3D v0.4.3

// 食べ物クラス
struct Food
{
	Vec2 pos;
};

void Main()
{
	// プレイヤーのテクスチャ
	const Texture playerTexture{ Emoji{ U"😋" } };

	// 食べ物のテクスチャ
	const Texture foodTexture{ Emoji{ U"🍰" } };

	// プレイヤーが毎秒何ピクセルの速さで移動するか
	double playerSpeed = 300.0;

	// 食べ物が毎秒何ピクセルの速さで落下するか
	double foodSpeed = 200.0;

	// 何秒ごとに食べ物が出現するか
	double foodSpawnTime = 1.0;

	// 前回の食べ物の出現から何秒経過したか
	double timeAccumulator = 0.0;

	// プレイヤーの位置
	Vec2 playerPos{ 400, 500 };

	// 食べ物
	Array<Food> foods;
	foods << Food{ .pos = Vec2{ 300, 100 } };
	foods << Food{ .pos = Vec2{ 500, 200 } };
	foods << Food{ .pos = Vec2{ 700, 300 } };

	while (System::Update())
	{
		// デバッグ表示
		{
			ClearPrint();
			Print << U"食べ物の個数: " << foods.size();
			Print << U"timeAccumulator: " << timeAccumulator;
		}

		// プレイヤーの移動
		{
			if (KeyLeft.pressed()) // 左矢印キーが押されていたら
			{
				playerPos.x -= (playerSpeed * Scene::DeltaTime());
			}
			else if (KeyRight.pressed()) // 右矢印キーが押されていたら
			{
				playerPos.x += (playerSpeed * Scene::DeltaTime());
			}

			// playerPos.x を 50 以上、750 以下に収める
			playerPos.x = Clamp(playerPos.x, 50.0, 750.0);
		}

		// 食べ物の出現
		{
			// 経過時間の蓄積
			timeAccumulator += Scene::DeltaTime();

			// 食べ物の出現時間よりも長く経過していた場合
			while (foodSpawnTime <= timeAccumulator)
			{
				// 食べ物をランダムな場所に出現させる
				foods << Food{ .pos = Vec2{ Random(50.0, 750.0), -50 } };

				// 経過時間を減らす
				timeAccumulator -= foodSpawnTime;
			}
		}

		// 食べ物の移動
		{
			const double move = (foodSpeed * Scene::DeltaTime());

			for (auto& food : foods)
			{
				food.pos.y += move;
			}
		}

		// 地面に落ちた食べ物の消去
		{
			// 条件を満たす要素を配列から削除(ラムダ式で記述)
			foods.remove_if([](const Food& food) { return (600 < food.pos.y); });
		}

		// プレイヤーによる食べ物の獲得
		{
			// プレイヤーのあたり判定円
			const Circle playerCircle{ playerPos, 60 };

			// イテレータで全ての食べ物をたどる
			for (auto it = foods.begin(); it != foods.end();)
			{
				// 食べ物のあたり判定円
				const Circle foodCircle{ it->pos, 30 };

				if (playerCircle.intersects(foodCircle)) // 交差していたら
				{
					it = foods.erase(it); // 要素を削除して次の要素へ
				}
				else
				{
					++it; // 次の要素へ
				}
			}
		}

		// 描画
		{
			// 空を描く(グラデーション)
			Rect{ 0, 0, 800, 520 }
				.draw(Arg::top = ColorF{ 0.1, 0.3, 0.6 }, Arg::bottom = ColorF{ 0.3, 0.7, 1.0 });

			// 地面を描く
			Rect{ 0, 520, 800, 80 }.draw(ColorF{ 0.2, 0.6, 0.3 });

			// プレイヤーを描く
			playerTexture.drawAt(playerPos);

			for (const auto& food : foods)
			{
				// 食べ物を描く(サイズは 0.5 倍)
				foodTexture.scaled(0.5).drawAt(food.pos);
			}
		}

		// あたり判定円のデバッグ表示
		{
			// プレイヤーのあたり判定円
			Circle{ playerPos, 60 }.draw(ColorF{ 1.0, 1.0, 1.0, 0.5 });

			for (const auto& food : foods)
			{
				// 食べ物のあたり判定円
				Circle{ food.pos, 30 }.draw(ColorF{ 1.0, 1.0, 1.0, 0.5 });
			}
		}
	}
}

14. 食べ物のバリエーションを増やす


落ちてくる食べ物のバリエーションを増やします。

食べ物のが何種類あるかを NumFoods という定数で決めます。そして、Food クラスに食べ物の種類を表すメンバ変数 type を追加し、0(NumFood - 1) の範囲の値を設定します。この値をインデックスとして使えるよう、食べ物のテクスチャの配列 std::array<Texture, NumFoods> を用意します。

# include <Siv3D.hpp> // OpenSiv3D v0.4.3

// 食べ物の種類
constexpr size_t NumFoods = 3;

// 食べ物クラス
struct Food
{
	Vec2 pos;

	size_t type = 0; // 0: 🍬, 1: 🍩, 2: 🍰
};

void Main()
{
	// プレイヤーのテクスチャ
	const Texture playerTexture{ Emoji{ U"😋" } };

	// 食べ物のテクスチャ
	const std::array<Texture, NumFoods> foodTextures =
	{
		Texture{ Emoji{ U"🍬" }},
		Texture{ Emoji{ U"🍩" }},
		Texture{ Emoji{ U"🍰" }},
	};

	// プレイヤーが毎秒何ピクセルの速さで移動するか
	double playerSpeed = 300.0;

	// 食べ物が毎秒何ピクセルの速さで落下するか
	double foodSpeed = 200.0;

	// 何秒ごとに食べ物が出現するか
	double foodSpawnTime = 1.0;

	// 前回の食べ物の出現から何秒経過したか
	double timeAccumulator = 0.0;

	// プレイヤーの位置
	Vec2 playerPos{ 400, 500 };

	// 食べ物
	Array<Food> foods;
	foods << Food{ .pos = Vec2{ 300, 100 }, .type = 2 };
	foods << Food{ .pos = Vec2{ 500, 200 }, .type = 1 };
	foods << Food{ .pos = Vec2{ 700, 300 }, .type = 0 };

	while (System::Update())
	{
		// デバッグ表示
		{
			ClearPrint();
			Print << U"食べ物の個数: " << foods.size();
			Print << U"timeAccumulator: " << timeAccumulator;
		}

		// プレイヤーの移動
		{
			if (KeyLeft.pressed()) // 左矢印キーが押されていたら
			{
				playerPos.x -= (playerSpeed * Scene::DeltaTime());
			}
			else if (KeyRight.pressed()) // 右矢印キーが押されていたら
			{
				playerPos.x += (playerSpeed * Scene::DeltaTime());
			}

			// playerPos.x を 50 以上、750 以下に収める
			playerPos.x = Clamp(playerPos.x, 50.0, 750.0);
		}

		// 食べ物の出現
		{
			// 経過時間の蓄積
			timeAccumulator += Scene::DeltaTime();

			// 食べ物の出現時間よりも長く経過していた場合
			while (foodSpawnTime <= timeAccumulator)
			{
				// 食べ物をランダムな場所に出現させる
				foods << Food{ .pos = Vec2{ Random(50.0, 750.0), -50 },
					.type = Random<size_t>(0, NumFoods - 1) };

				// 経過時間を減らす
				timeAccumulator -= foodSpawnTime;
			}
		}

		// 食べ物の移動
		{
			const double move = (foodSpeed * Scene::DeltaTime());

			for (auto& food : foods)
			{
				food.pos.y += move;
			}
		}

		// 地面に落ちた食べ物の消去
		{
			// 条件を満たす要素を配列から削除(ラムダ式で記述)
			foods.remove_if([](const Food& food) { return (600 < food.pos.y); });
		}

		// プレイヤーによる食べ物の獲得
		{
			// プレイヤーのあたり判定円
			const Circle playerCircle{ playerPos, 60 };

			// イテレータで全ての食べ物をたどる
			for (auto it = foods.begin(); it != foods.end();)
			{
				// 食べ物のあたり判定円
				const Circle foodCircle{ it->pos, 30 };

				if (playerCircle.intersects(foodCircle)) // 交差していたら
				{
					it = foods.erase(it); // 要素を削除して次の要素へ
				}
				else
				{
					++it; // 次の要素へ
				}
			}
		}

		// 描画
		{
			// 空を描く(グラデーション)
			Rect{ 0, 0, 800, 520 }
				.draw(Arg::top = ColorF{ 0.1, 0.3, 0.6 }, Arg::bottom = ColorF{ 0.3, 0.7, 1.0 });

			// 地面を描く
			Rect{ 0, 520, 800, 80 }.draw(ColorF{ 0.2, 0.6, 0.3 });

			// プレイヤーを描く
			playerTexture.drawAt(playerPos);

			for (const auto& food : foods)
			{
				// 食べ物を描く(サイズは 0.5 倍)
				foodTextures[food.type].scaled(0.5).drawAt(food.pos);
			}
		}
	}
}

15. 食べ物にスコアを付ける

スコアを導入して、食べ物に応じて異なるスコアが加算されるようにしましょう。

Foodtype をインデックスとしてアクセスできる、獲得スコアの配列 std::array<int32, NumFoods> を用意します。

# include <Siv3D.hpp> // OpenSiv3D v0.4.3

// 食べ物の種類
constexpr size_t NumFoods = 3;

// 食べ物クラス
struct Food
{
	Vec2 pos;

	size_t type = 0; // 0: 🍬, 1: 🍩, 2: 🍰
};

void Main()
{
	// プレイヤーのテクスチャ
	const Texture playerTexture{ Emoji{ U"😋" } };

	// 食べ物のテクスチャ
	const std::array<Texture, NumFoods> foodTextures =
	{
		Texture{ Emoji{ U"🍬" }},
		Texture{ Emoji{ U"🍩" }},
		Texture{ Emoji{ U"🍰" }},
	};

	// 食べ物の獲得スコア
	const std::array<int32, NumFoods> foodScores =
	{
		10, 20, 100
	};

	// プレイヤーが毎秒何ピクセルの速さで移動するか
	double playerSpeed = 300.0;

	// 食べ物が毎秒何ピクセルの速さで落下するか
	double foodSpeed = 200.0;

	// 何秒ごとに食べ物が出現するか
	double foodSpawnTime = 1.0;

	// 前回の食べ物の出現から何秒経過したか
	double timeAccumulator = 0.0;

	// プレイヤーの位置
	Vec2 playerPos{ 400, 500 };

	// スコア
	int32 score = 0;

	// 食べ物
	Array<Food> foods;
	foods << Food{ .pos = Vec2{ 300, 100 }, .type = 2 };
	foods << Food{ .pos = Vec2{ 500, 200 }, .type = 1 };
	foods << Food{ .pos = Vec2{ 700, 300 }, .type = 0 };

	while (System::Update())
	{
		// デバッグ表示
		{
			ClearPrint();
			Print << U"食べ物の個数: " << foods.size();
			Print << U"timeAccumulator: " << timeAccumulator;
			Print << U"スコア: " << score;
		}

		// プレイヤーの移動
		{
			if (KeyLeft.pressed()) // 左矢印キーが押されていたら
			{
				playerPos.x -= (playerSpeed * Scene::DeltaTime());
			}
			else if (KeyRight.pressed()) // 右矢印キーが押されていたら
			{
				playerPos.x += (playerSpeed * Scene::DeltaTime());
			}

			// playerPos.x を 50 以上、750 以下に収める
			playerPos.x = Clamp(playerPos.x, 50.0, 750.0);
		}

		// 食べ物の出現
		{
			// 経過時間の蓄積
			timeAccumulator += Scene::DeltaTime();

			// 食べ物の出現時間よりも長く経過していた場合
			while (foodSpawnTime <= timeAccumulator)
			{
				// 食べ物をランダムな場所に出現させる
				foods << Food{ .pos = Vec2{ Random(50.0, 750.0), -50 },
					.type = Random<size_t>(0, NumFoods - 1) };

				// 経過時間を減らす
				timeAccumulator -= foodSpawnTime;
			}
		}

		// 食べ物の移動
		{
			const double move = (foodSpeed * Scene::DeltaTime());

			for (auto& food : foods)
			{
				food.pos.y += move;
			}
		}

		// 地面に落ちた食べ物の消去
		{
			// 条件を満たす要素を配列から削除(ラムダ式で記述)
			foods.remove_if([](const Food& food) { return (600 < food.pos.y); });
		}

		// プレイヤーによる食べ物の獲得
		{
			// プレイヤーのあたり判定円
			const Circle playerCircle{ playerPos, 60 };

			// イテレータで全ての食べ物をたどる
			for (auto it = foods.begin(); it != foods.end();)
			{
				// 食べ物のあたり判定円
				const Circle foodCircle{ it->pos, 30 };

				if (playerCircle.intersects(foodCircle)) // 交差していたら
				{
					score += foodScores[it->type]; // スコアを加算(要素を削除する前に it を使うこと!)

					it = foods.erase(it); // 要素を削除して次の要素へ
				}
				else
				{
					++it; // 次の要素へ
				}
			}
		}

		// 描画
		{
			// 空を描く(グラデーション)
			Rect{ 0, 0, 800, 520 }
				.draw(Arg::top = ColorF{ 0.1, 0.3, 0.6 }, Arg::bottom = ColorF{ 0.3, 0.7, 1.0 });

			// 地面を描く
			Rect{ 0, 520, 800, 80 }.draw(ColorF{ 0.2, 0.6, 0.3 });

			// プレイヤーを描く
			playerTexture.drawAt(playerPos);

			for (const auto& food : foods)
			{
				// 食べ物を描く(サイズは 0.5 倍)
				foodTextures[food.type].scaled(0.5).drawAt(food.pos);
			}
		}
	}
}

▶ Day 3 に続く(準備中)

Discussion