コンテンツにスキップ

1. クッキークリッカーの改良

1.1 桁区切り記号を付ける

  • 整数に桁区切り記号を付けるには String ThousandsSeparate(value, separator) を使います
# include <Siv3D.hpp>

void Main()
{
	Print << 123456789;

	Print << ThousandsSeparate(123456789);

	Print << ThousandsSeparate(123456789, U" ");

	while (System::Update())
	{

	}
}
  • クッキーの枚数に桁区切り記号を付けます

# include <Siv3D.hpp>

/// @brief アイテムのボタン
/// @param rect ボタンの領域
/// @param texture ボタンの絵文字
/// @param font 文字描画に使うフォント
/// @param name アイテムの名前
/// @param desc アイテムの説明
/// @param count アイテムの所持数
/// @param enabled ボタンを押せるか
/// @return ボタンが押された場合 true, それ以外の場合は false
bool Button(const Rect& rect, const Texture& texture, const Font& font, const String& name, const String& desc, int32 count, bool enabled)
{
	if (enabled)
	{
		rect.draw(ColorF{ 0.3, 0.5, 0.9, 0.8 });

		rect.drawFrame(2, 2, ColorF{ 0.5, 0.7, 1.0 });

		if (rect.mouseOver())
		{
			Cursor::RequestStyle(CursorStyle::Hand);
		}
	}
	else
	{
		rect.draw(ColorF{ 0.0, 0.4 });

		rect.drawFrame(2, 2, ColorF{ 0.5 });
	}

	texture.scaled(0.5).drawAt(rect.x + 50, rect.y + 50);

	font(name).draw(30, rect.x + 100, rect.y + 15, Palette::White);

	font(desc).draw(18, rect.x + 102, rect.y + 60, Palette::White);

	font(count).draw(50, Arg::rightCenter((rect.x + rect.w - 20), (rect.y + 50)), Palette::White);

	return (enabled && rect.leftClicked());
}

void Main()
{
	// クッキーの絵文字
	Texture texture{ U"🍪"_emoji };

	// 農場の絵文字
	Texture farmEmoji{ U"🌾"_emoji };

	// 工場の絵文字
	Texture factoryEmoji{ U"🏭"_emoji };

	// フォント
	Font font{ FontMethod::MSDF, 48, Typeface::Bold };

	// クッキーのクリック円
	Circle cookieCircle{ 170, 300, 100 };

	// クッキーの表示サイズ(倍率)
	double cookieScale = 1.5;

	// クッキーの個数
	double cookies = 0;

	// 農場の所有数
	int32 farmCount = 0;

	// 工場の所有数
	int32 factoryCount = 0;

	// 農場の価格
	int32 farmCost = 10;

	// 工場の価格
	int32 factoryCost = 100;

	// クッキーの毎秒の生産量
	int32 cps = 0;

	// ゲームの経過時間の蓄積
	double accumulatedTime = 0.0;

	while (System::Update())
	{
		// クッキーの毎秒の生産量を計算する
		cps = (farmCount + factoryCount * 10);

		// ゲームの経過時間を加算する
		accumulatedTime += Scene::DeltaTime();

		// 0.1 秒以上蓄積していたら
		if (0.1 <= accumulatedTime)
		{
			accumulatedTime -= 0.1;

			// 0.1 秒分のクッキー生産を加算する
			cookies += (cps * 0.1);
		}

		// 農場の価格を計算する
		farmCost = 10 + (farmCount * 10);

		// 工場の価格を計算する
		factoryCost = 100 + (factoryCount * 100);

		// クッキー円上にマウスカーソルがあれば
		if (cookieCircle.mouseOver())
		{
			Cursor::RequestStyle(CursorStyle::Hand);
		}

		// クッキー円が左クリックされたら
		if (cookieCircle.leftClicked())
		{
			cookieScale = 1.3;
			++cookies;
		}

		// クッキーの表示サイズを回復する
		cookieScale += Scene::DeltaTime();

		if (1.5 < cookieScale)
		{
			cookieScale = 1.5;
		}

		// 背景を描く
		Rect{ 0, 0, 800, 600 }.draw(Arg::top = ColorF{ 0.6, 0.5, 0.3 }, Arg::bottom = ColorF{ 0.2, 0.5, 0.3 });

		// クッキーの数を整数で表示する
		font(ThousandsSeparate((int32)cookies)).drawAt(60, 170, 100);

		// クッキーの生産量を表示する
		font(U"毎秒: {}"_fmt(cps)).drawAt(24, 170, 160);

		// クッキーを描画する
		texture.scaled(cookieScale).drawAt(cookieCircle.x, cookieCircle.y);

		// 農場ボタン
		if (Button(Rect{ 340, 40, 420, 100 }, farmEmoji, font, U"クッキー農場", U"C{} / 1 CPS"_fmt(farmCost), farmCount, (farmCost <= cookies)))
		{
			cookies -= farmCost;
			++farmCount;
		}

		// 工場ボタン
		if (Button(Rect{ 340, 160, 420, 100 }, factoryEmoji, font, U"クッキー工場", U"C{} / 10 CPS"_fmt(factoryCost), factoryCount, (factoryCost <= cookies)))
		{
			cookies -= factoryCost;
			++factoryCount;
		}
	}
}

1.2 押し心地を改良する

  • ばねの動きを次のように再現できます
# include <Siv3D.hpp>

void Main()
{
	// ばねの伸び
	double springX = 0.0;

	// ばねの速度
	double springVelocity = 0.0;

	// ばねの蓄積時間
	double springAccumulatedTime = 0.0;

	while (System::Update())
	{
		springAccumulatedTime += Scene::DeltaTime();

		while (0.005 <= springAccumulatedTime)
		{
			// ばねの力(変化を打ち消す方向)
			double force = (-0.02 * springX);

			// 画面を押しているときに働く力
			if (MouseL.pressed())
			{
				force += 4.0;
			}

			// 速度に力を適用(減衰もさせる)
			springVelocity = (springVelocity + force) * 0.92;

			// 位置に反映
			springX += springVelocity;

			springAccumulatedTime -= 0.005;
		}

		Circle{ 400 + springX, 300, 40 }.draw();
	}
}
  • クッキーを押したときの大きさの変化をばねの動きに対応させます
# include <Siv3D.hpp>

/// @brief アイテムのボタン
/// @param rect ボタンの領域
/// @param texture ボタンの絵文字
/// @param font 文字描画に使うフォント
/// @param name アイテムの名前
/// @param desc アイテムの説明
/// @param count アイテムの所持数
/// @param enabled ボタンを押せるか
/// @return ボタンが押された場合 true, それ以外の場合は false
bool Button(const Rect& rect, const Texture& texture, const Font& font, const String& name, const String& desc, int32 count, bool enabled)
{
	if (enabled)
	{
		rect.draw(ColorF{ 0.3, 0.5, 0.9, 0.8 });

		rect.drawFrame(2, 2, ColorF{ 0.5, 0.7, 1.0 });

		if (rect.mouseOver())
		{
			Cursor::RequestStyle(CursorStyle::Hand);
		}
	}
	else
	{
		rect.draw(ColorF{ 0.0, 0.4 });

		rect.drawFrame(2, 2, ColorF{ 0.5 });
	}

	texture.scaled(0.5).drawAt(rect.x + 50, rect.y + 50);

	font(name).draw(30, rect.x + 100, rect.y + 15, Palette::White);

	font(desc).draw(18, rect.x + 102, rect.y + 60, Palette::White);

	font(count).draw(50, Arg::rightCenter((rect.x + rect.w - 20), (rect.y + 50)), Palette::White);

	return (enabled && rect.leftClicked());
}

void Main()
{
	// クッキーの絵文字
	Texture texture{ U"🍪"_emoji };

	// 農場の絵文字
	Texture farmEmoji{ U"🌾"_emoji };

	// 工場の絵文字
	Texture factoryEmoji{ U"🏭"_emoji };

	// フォント
	Font font{ FontMethod::MSDF, 48, Typeface::Bold };

	// クッキーのクリック円
	Circle cookieCircle{ 170, 300, 100 };

	// ばねの伸び
	double springX = 0.0;

	// ばねの速度
	double springVelocity = 0.0;

	// ばねの蓄積時間
	double springAccumulatedTime = 0.0;

	// クッキーの個数
	double cookies = 0;

	// 農場の所有数
	int32 farmCount = 0;

	// 工場の所有数
	int32 factoryCount = 0;

	// 農場の価格
	int32 farmCost = 10;

	// 工場の価格
	int32 factoryCost = 100;

	// クッキーの毎秒の生産量
	int32 cps = 0;

	// ゲームの経過時間の蓄積
	double accumulatedTime = 0.0;

	while (System::Update())
	{
		// クッキーの毎秒の生産量を計算する
		cps = (farmCount + factoryCount * 10);

		// ゲームの経過時間を加算する
		accumulatedTime += Scene::DeltaTime();

		// 0.1 秒以上蓄積していたら
		if (0.1 <= accumulatedTime)
		{
			accumulatedTime -= 0.1;

			// 0.1 秒分のクッキー生産を加算する
			cookies += (cps * 0.1);
		}

		// ばねの蓄積時間を加算する
		springAccumulatedTime += Scene::DeltaTime();

		while (0.005 <= springAccumulatedTime)
		{
			// ばねの力(変化を打ち消す方向)
			double force = (-0.02 * springX);

			// 画面を押しているときに働く力
			if (cookieCircle.leftPressed())
			{
				force += 0.004;
			}

			// 速度に力を適用(減衰もさせる)
			springVelocity = (springVelocity + force) * 0.92;

			// 位置に反映
			springX += springVelocity;

			springAccumulatedTime -= 0.005;
		}

		// 農場の価格を計算する
		farmCost = 10 + (farmCount * 10);

		// 工場の価格を計算する
		factoryCost = 100 + (factoryCount * 100);

		// クッキー円上にマウスカーソルがあれば
		if (cookieCircle.mouseOver())
		{
			Cursor::RequestStyle(CursorStyle::Hand);
		}

		// クッキー円が左クリックされたら
		if (cookieCircle.leftClicked())
		{
			++cookies;
		}

		// 背景を描く
		Rect{ 0, 0, 800, 600 }.draw(Arg::top = ColorF{ 0.6, 0.5, 0.3 }, Arg::bottom = ColorF{ 0.2, 0.5, 0.3 });

		// クッキーの数を整数で表示する
		font(ThousandsSeparate((int32)cookies)).drawAt(60, 170, 100);

		// クッキーの生産量を表示する
		font(U"毎秒: {}"_fmt(cps)).drawAt(24, 170, 160);

		// クッキーを描画する
		texture.scaled(1.5 - springX).drawAt(cookieCircle.x, cookieCircle.y);

		// 農場ボタン
		if (Button(Rect{ 340, 40, 420, 100 }, farmEmoji, font, U"クッキー農場", U"C{} / 1 CPS"_fmt(farmCost), farmCount, (farmCost <= cookies)))
		{
			cookies -= farmCost;
			++farmCount;
		}

		// 工場ボタン
		if (Button(Rect{ 340, 160, 420, 100 }, factoryEmoji, font, U"クッキー工場", U"C{} / 10 CPS"_fmt(factoryCost), factoryCount, (factoryCost <= cookies)))
		{
			cookies -= factoryCost;
			++factoryCount;
		}
	}
}

1.3 クリック時のエフェクトを追加する

  • Effect を使うと、複数フレーム間にまたがるアニメーションを簡単に記述できます
  • クリックしたときにクッキーが舞うエフェクトや、「+1」という文字が上昇するエフェクトを作ります 

1.3.1 エフェクトの基本

エフェクト機能を使うには、エフェクトを管理する Effect オブジェクトを作成し、IEffect を継承した任意のクラス EffectType を使って、Effect::add<EffectType>() を通してアクティブなエフェクトを追加します。

IEffect を継承するクラスに必要なメンバ関数の実装は bool update(double t) override です。

この関数は、エフェクトが発生してからの経過時間 t を受け取り、それに応じたエフェクトの描画を行います。戻り値として、エフェクトを次のフレームも継続させる場合 true を、削除する場合は false を返します。例えば return (t < 3.0); とすれば、エフェクトは 3 秒間継続することになります。

Effect は、時間ベースのアニメーションを簡単に作れるため、ゲームの演出などで重宝します。次のプログラムは、クリックした場所に、時間とともに大きくなる輪を 1 秒にわたって発生させるエフェクトの実装例です。

# include <Siv3D.hpp>

struct RingEffect : IEffect
{
	Vec2 m_pos;

	ColorF m_color;

	// このコンストラクタ引数が、Effect::add<RingEffect>() の引数になる
	explicit RingEffect(const Vec2& pos)
		: m_pos{ pos }
		, m_color{ RandomColorF() } {}

	bool update(double t) override
	{
		// 時間に応じて大きくなる輪
		Circle{ m_pos, (t * 100) }.drawFrame(4, m_color);

		// 1 秒未満なら継続
		return (t < 1.0);
	}
};

void Main()
{
	Effect effect;

	while (System::Update())
	{
		ClearPrint();

		// アクティブなエフェクトの数
		Print << U"Active effects: {}"_fmt(effect.num_effects());

		if (MouseL.down())
		{
			// エフェクトを発生
			effect.add<RingEffect>(Cursor::Pos());
		}

		// アクティブなエフェクトのプログラム IEffect::update() を実行
		effect.update();
	}
}

1.3.2 クッキークリッカー用のエフェクト

# include <Siv3D.hpp>

// クッキーが舞うエフェクト
struct CookieEffect : IEffect
{
	// 初期座標
	Vec2 m_start;

	// 初速
	Vec2 m_velocity;

	// 拡大倍率
	double m_scale;

	// 回転角度
	double m_angle;

	// テクスチャ
	Texture m_texture;

	CookieEffect(const Vec2& start, const Texture& texture)
		: m_start{ start }
		, m_velocity{ Circular{ 80, Random(-40_deg, 40_deg) } }
		, m_scale{ Random(0.2, 0.3) }
		, m_angle{ Random(2_pi) }
		, m_texture{ texture } {}

	bool update(double t) override
	{
		const Vec2 pos = m_start
			+ m_velocity * t + 0.5 * t * t * Vec2{ 0, 120 };

		m_texture.scaled(m_scale).rotated(m_angle).drawAt(pos, ColorF{1.0, (1.0 - t)});

		return (t < 1.0);
	}
};

// 「+1」が上昇するエフェクト
struct PlusOneEffect : IEffect
{
	// 初期座標
	Vec2 m_start;

	// フォント
	Font m_font;

	PlusOneEffect(const Vec2& start, const Font& font)
		: m_start{ start }
		, m_font{ font } {}

	bool update(double t) override
	{
		m_font(U"+1").drawAt(24, m_start.movedBy(0, t * -120), ColorF{ 1.0, (1.0 - t) });

		return (t < 1.0);
	}
};

void Main()
{
	Texture texture{ U"🍪"_emoji };

	Font font{ FontMethod::MSDF, 48, Typeface::Bold };

	Effect effect;

	while (System::Update())
	{
		if (MouseL.down())
		{
			effect.add<CookieEffect>(Cursor::Pos().movedBy(Random(-5, 5), Random(-5, 5)), texture);

			effect.add<PlusOneEffect>(Cursor::Pos().movedBy(Random(-5, 5), Random(-15, -5)), font);
		}

		effect.update();
	}
}

# include <Siv3D.hpp>

/// @brief アイテムのボタン
/// @param rect ボタンの領域
/// @param texture ボタンの絵文字
/// @param font 文字描画に使うフォント
/// @param name アイテムの名前
/// @param desc アイテムの説明
/// @param count アイテムの所持数
/// @param enabled ボタンを押せるか
/// @return ボタンが押された場合 true, それ以外の場合は false
bool Button(const Rect& rect, const Texture& texture, const Font& font, const String& name, const String& desc, int32 count, bool enabled)
{
	if (enabled)
	{
		rect.draw(ColorF{ 0.3, 0.5, 0.9, 0.8 });

		rect.drawFrame(2, 2, ColorF{ 0.5, 0.7, 1.0 });

		if (rect.mouseOver())
		{
			Cursor::RequestStyle(CursorStyle::Hand);
		}
	}
	else
	{
		rect.draw(ColorF{ 0.0, 0.4 });

		rect.drawFrame(2, 2, ColorF{ 0.5 });
	}

	texture.scaled(0.5).drawAt(rect.x + 50, rect.y + 50);

	font(name).draw(30, rect.x + 100, rect.y + 15, Palette::White);

	font(desc).draw(18, rect.x + 102, rect.y + 60, Palette::White);

	font(count).draw(50, Arg::rightCenter((rect.x + rect.w - 20), (rect.y + 50)), Palette::White);

	return (enabled && rect.leftClicked());
}

// クッキーが舞うエフェクト
struct CookieEffect : IEffect
{
	// 初期座標
	Vec2 m_start;

	// 初速
	Vec2 m_velocity;

	// 拡大倍率
	double m_scale;

	// 回転角度
	double m_angle;

	// テクスチャ
	Texture m_texture;

	CookieEffect(const Vec2& start, const Texture& texture)
		: m_start{ start }
		, m_velocity{ Circular{ 80, Random(-40_deg, 40_deg) } }
		, m_scale{ Random(0.2, 0.3) }
		, m_angle{ Random(2_pi) }
		, m_texture{ texture } {}

	bool update(double t) override
	{
		const Vec2 pos = m_start
			+ m_velocity * t + 0.5 * t * t * Vec2{ 0, 120 };

		m_texture.scaled(m_scale).rotated(m_angle).drawAt(pos, ColorF{ 1.0, (1.0 - t) });

		return (t < 1.0);
	}
};

// 「+1」が上昇するエフェクト
struct PlusOneEffect : IEffect
{
	// 初期座標
	Vec2 m_start;

	// フォント
	Font m_font;

	PlusOneEffect(const Vec2& start, const Font& font)
		: m_start{ start }
		, m_font{ font } {}

	bool update(double t) override
	{
		m_font(U"+1").drawAt(24, m_start.movedBy(0, t * -120), ColorF{ 1.0, (1.0 - t) });

		return (t < 1.0);
	}
};

void Main()
{
	// クッキーの絵文字
	Texture texture{ U"🍪"_emoji };

	// 農場の絵文字
	Texture farmEmoji{ U"🌾"_emoji };

	// 工場の絵文字
	Texture factoryEmoji{ U"🏭"_emoji };

	// フォント
	Font font{ FontMethod::MSDF, 48, Typeface::Bold };

	// エフェクト
	Effect effect;

	// クッキーのクリック円
	Circle cookieCircle{ 170, 300, 100 };

	// ばねの伸び
	double springX = 0.0;

	// ばねの速度
	double springVelocity = 0.0;

	// ばねの蓄積時間
	double springAccumulatedTime = 0.0;

	// クッキーの個数
	double cookies = 0;

	// 農場の所有数
	int32 farmCount = 0;

	// 工場の所有数
	int32 factoryCount = 0;

	// 農場の価格
	int32 farmCost = 10;

	// 工場の価格
	int32 factoryCost = 100;

	// クッキーの毎秒の生産量
	int32 cps = 0;

	// ゲームの経過時間の蓄積
	double accumulatedTime = 0.0;

	while (System::Update())
	{
		// クッキーの毎秒の生産量を計算する
		cps = (farmCount + factoryCount * 10);

		// ゲームの経過時間を加算する
		accumulatedTime += Scene::DeltaTime();

		// 0.1 秒以上蓄積していたら
		if (0.1 <= accumulatedTime)
		{
			accumulatedTime -= 0.1;

			// 0.1 秒分のクッキー生産を加算する
			cookies += (cps * 0.1);
		}

		// ばねの蓄積時間を加算する
		springAccumulatedTime += Scene::DeltaTime();

		while (0.005 <= springAccumulatedTime)
		{
			// ばねの力(変化を打ち消す方向)
			double force = (-0.02 * springX);

			// 画面を押しているときに働く力
			if (cookieCircle.leftPressed())
			{
				force += 0.004;
			}

			// 速度に力を適用(減衰もさせる)
			springVelocity = (springVelocity + force) * 0.92;

			// 位置に反映
			springX += springVelocity;

			springAccumulatedTime -= 0.005;
		}

		// 農場の価格を計算する
		farmCost = 10 + (farmCount * 10);

		// 工場の価格を計算する
		factoryCost = 100 + (factoryCount * 100);

		// クッキー円上にマウスカーソルがあれば
		if (cookieCircle.mouseOver())
		{
			Cursor::RequestStyle(CursorStyle::Hand);
		}

		// クッキー円が左クリックされたら
		if (cookieCircle.leftClicked())
		{
			++cookies;

			// クッキーが舞うエフェクトを追加する
			effect.add<CookieEffect>(Cursor::Pos().movedBy(Random(-5, 5), Random(-5, 5)), texture);

			// 「+1」が上昇するエフェクトを追加する
			effect.add<PlusOneEffect>(Cursor::Pos().movedBy(Random(-5, 5), Random(-15, -5)), font);
		}

		// 背景を描く
		Rect{ 0, 0, 800, 600 }.draw(Arg::top = ColorF{ 0.6, 0.5, 0.3 }, Arg::bottom = ColorF{ 0.2, 0.5, 0.3 });

		// クッキーの数を整数で表示する
		font(ThousandsSeparate((int32)cookies)).drawAt(60, 170, 100);

		// クッキーの生産量を表示する
		font(U"毎秒: {}"_fmt(cps)).drawAt(24, 170, 160);

		// クッキーを描画する
		texture.scaled(1.5 - springX).drawAt(cookieCircle.x, cookieCircle.y);

		// エフェクトを描画する
		effect.update();

		// 農場ボタン
		if (Button(Rect{ 340, 40, 420, 100 }, farmEmoji, font, U"クッキー農場", U"C{} / 1 CPS"_fmt(farmCost), farmCount, (farmCost <= cookies)))
		{
			cookies -= farmCost;
			++farmCount;
		}

		// 工場ボタン
		if (Button(Rect{ 340, 160, 420, 100 }, factoryEmoji, font, U"クッキー工場", U"C{} / 10 CPS"_fmt(factoryCost), factoryCount, (factoryCost <= cookies)))
		{
			cookies -= factoryCost;
			++factoryCount;
		}
	}
}

1.4 クッキーの後光エフェクトを追加する

  • Circle::drawPie(startAngle, angle, innerColor, outerColor) を使って後光を描きます
# include <Siv3D.hpp>

void Main()
{
	while (System::Update())
	{
		for (int32 i = 0; i < 4; ++i)
		{
			double startAngle = Scene::Time() * 15_deg + i * 90_deg;
			Circle{ 400, 300, 200 }.drawPie(startAngle, 60_deg, ColorF{ 1.0, 0.5 }, ColorF{ 1.0, 0.0 });
		}

		for (int32 i = 0; i < 6; ++i)
		{
			double startAngle = Scene::Time() * -15_deg + i * 60_deg;
			Circle{ 400, 300, 200 }.drawPie(startAngle, 40_deg, ColorF{ 1.0, 0.5 }, ColorF{ 1.0, 0.0 });
		}
	}
}

# include <Siv3D.hpp>

/// @brief アイテムのボタン
/// @param rect ボタンの領域
/// @param texture ボタンの絵文字
/// @param font 文字描画に使うフォント
/// @param name アイテムの名前
/// @param desc アイテムの説明
/// @param count アイテムの所持数
/// @param enabled ボタンを押せるか
/// @return ボタンが押された場合 true, それ以外の場合は false
bool Button(const Rect& rect, const Texture& texture, const Font& font, const String& name, const String& desc, int32 count, bool enabled)
{
	if (enabled)
	{
		rect.draw(ColorF{ 0.3, 0.5, 0.9, 0.8 });

		rect.drawFrame(2, 2, ColorF{ 0.5, 0.7, 1.0 });

		if (rect.mouseOver())
		{
			Cursor::RequestStyle(CursorStyle::Hand);
		}
	}
	else
	{
		rect.draw(ColorF{ 0.0, 0.4 });

		rect.drawFrame(2, 2, ColorF{ 0.5 });
	}

	texture.scaled(0.5).drawAt(rect.x + 50, rect.y + 50);

	font(name).draw(30, rect.x + 100, rect.y + 15, Palette::White);

	font(desc).draw(18, rect.x + 102, rect.y + 60, Palette::White);

	font(count).draw(50, Arg::rightCenter((rect.x + rect.w - 20), (rect.y + 50)), Palette::White);

	return (enabled && rect.leftClicked());
}

// クッキーが舞うエフェクト
struct CookieEffect : IEffect
{
	// 初期座標
	Vec2 m_start;

	// 初速
	Vec2 m_velocity;

	// 拡大倍率
	double m_scale;

	// 回転角度
	double m_angle;

	// テクスチャ
	Texture m_texture;

	CookieEffect(const Vec2& start, const Texture& texture)
		: m_start{ start }
		, m_velocity{ Circular{ 80, Random(-40_deg, 40_deg) } }
		, m_scale{ Random(0.2, 0.3) }
		, m_angle{ Random(2_pi) }
		, m_texture{ texture } {}

	bool update(double t) override
	{
		const Vec2 pos = m_start
			+ m_velocity * t + 0.5 * t * t * Vec2{ 0, 120 };

		m_texture.scaled(m_scale).rotated(m_angle).drawAt(pos, ColorF{ 1.0, (1.0 - t) });

		return (t < 1.0);
	}
};

// 「+1」が上昇するエフェクト
struct PlusOneEffect : IEffect
{
	// 初期座標
	Vec2 m_start;

	// フォント
	Font m_font;

	PlusOneEffect(const Vec2& start, const Font& font)
		: m_start{ start }
		, m_font{ font } {}

	bool update(double t) override
	{
		m_font(U"+1").drawAt(24, m_start.movedBy(0, t * -120), ColorF{ 1.0, (1.0 - t) });

		return (t < 1.0);
	}
};

void Main()
{
	// クッキーの絵文字
	Texture texture{ U"🍪"_emoji };

	// 農場の絵文字
	Texture farmEmoji{ U"🌾"_emoji };

	// 工場の絵文字
	Texture factoryEmoji{ U"🏭"_emoji };

	// フォント
	Font font{ FontMethod::MSDF, 48, Typeface::Bold };

	// エフェクト
	Effect effect;

	// クッキーのクリック円
	Circle cookieCircle{ 170, 300, 100 };

	// ばねの伸び
	double springX = 0.0;

	// ばねの速度
	double springVelocity = 0.0;

	// ばねの蓄積時間
	double springAccumulatedTime = 0.0;

	// クッキーの個数
	double cookies = 0;

	// 農場の所有数
	int32 farmCount = 0;

	// 工場の所有数
	int32 factoryCount = 0;

	// 農場の価格
	int32 farmCost = 10;

	// 工場の価格
	int32 factoryCost = 100;

	// クッキーの毎秒の生産量
	int32 cps = 0;

	// ゲームの経過時間の蓄積
	double accumulatedTime = 0.0;

	while (System::Update())
	{
		// クッキーの毎秒の生産量を計算する
		cps = (farmCount + factoryCount * 10);

		// ゲームの経過時間を加算する
		accumulatedTime += Scene::DeltaTime();

		// 0.1 秒以上蓄積していたら
		if (0.1 <= accumulatedTime)
		{
			accumulatedTime -= 0.1;

			// 0.1 秒分のクッキー生産を加算する
			cookies += (cps * 0.1);
		}

		// ばねの蓄積時間を加算する
		springAccumulatedTime += Scene::DeltaTime();

		while (0.005 <= springAccumulatedTime)
		{
			// ばねの力(変化を打ち消す方向)
			double force = (-0.02 * springX);

			// 画面を押しているときに働く力
			if (cookieCircle.leftPressed())
			{
				force += 0.004;
			}

			// 速度に力を適用(減衰もさせる)
			springVelocity = (springVelocity + force) * 0.92;

			// 位置に反映
			springX += springVelocity;

			springAccumulatedTime -= 0.005;
		}

		// 農場の価格を計算する
		farmCost = 10 + (farmCount * 10);

		// 工場の価格を計算する
		factoryCost = 100 + (factoryCount * 100);

		// クッキー円上にマウスカーソルがあれば
		if (cookieCircle.mouseOver())
		{
			Cursor::RequestStyle(CursorStyle::Hand);
		}

		// クッキー円が左クリックされたら
		if (cookieCircle.leftClicked())
		{
			++cookies;

			// クッキーが舞うエフェクトを追加する
			effect.add<CookieEffect>(Cursor::Pos().movedBy(Random(-5, 5), Random(-5, 5)), texture);

			// 「+1」が上昇するエフェクトを追加する
			effect.add<PlusOneEffect>(Cursor::Pos().movedBy(Random(-5, 5), Random(-15, -5)), font);
		}

		// 背景を描く
		Rect{ 0, 0, 800, 600 }.draw(Arg::top = ColorF{ 0.6, 0.5, 0.3 }, Arg::bottom = ColorF{ 0.2, 0.5, 0.3 });

		// クッキーの後光を描く
		{
			for (int32 i = 0; i < 4; ++i)
			{
				double startAngle = Scene::Time() * 15_deg + i * 90_deg;
				Circle{ cookieCircle.center, 180 }.drawPie(startAngle, 60_deg, ColorF{ 1.0, 0.3 }, ColorF{ 1.0, 0.0 });
			}

			for (int32 i = 0; i < 6; ++i)
			{
				double startAngle = Scene::Time() * -15_deg + i * 60_deg;
				Circle{ cookieCircle.center, 180 }.drawPie(startAngle, 40_deg, ColorF{ 1.0, 0.3 }, ColorF{ 1.0, 0.0 });
			}
		}

		// クッキーの数を整数で表示する
		font(ThousandsSeparate((int32)cookies)).drawAt(60, 170, 100);

		// クッキーの生産量を表示する
		font(U"毎秒: {}"_fmt(cps)).drawAt(24, 170, 160);

		// クッキーを描画する
		texture.scaled(1.5 - springX).drawAt(cookieCircle.x, cookieCircle.y);

		// エフェクトを描画する
		effect.update();

		// 農場ボタン
		if (Button(Rect{ 340, 40, 420, 100 }, farmEmoji, font, U"クッキー農場", U"C{} / 1 CPS"_fmt(farmCost), farmCount, (farmCost <= cookies)))
		{
			cookies -= farmCost;
			++farmCount;
		}

		// 工場ボタン
		if (Button(Rect{ 340, 160, 420, 100 }, factoryEmoji, font, U"クッキー工場", U"C{} / 10 CPS"_fmt(factoryCost), factoryCount, (factoryCost <= cookies)))
		{
			cookies -= factoryCost;
			++factoryCount;
		}
	}
}

1.5 クッキーが降り注ぐ演出を付ける

  • Effect を使って背景にクッキーを降らせます
# include <Siv3D.hpp>

// クッキーが降るエフェクト
struct CookieBackgroundEffect : IEffect
{
	// 初期座標
	Vec2 m_start;

	// 回転角度
	double m_angle;

	// テクスチャ
	Texture m_texture;

	CookieBackgroundEffect(const Vec2& start, const Texture& texture)
		: m_start{ start }
		, m_angle{ Random(2_pi) }
		, m_texture{ texture } {}

	bool update(double t) override
	{
		const Vec2 pos = m_start + 0.5 * t * t * Vec2{ 0, 120 };

		m_texture.scaled(0.3).rotated(m_angle).drawAt(pos, ColorF{ 1.0, (1.0 - t / 3.0) });

		return (t < 3.0);
	}
};

void Main()
{
	// クッキーの絵文字
	Texture texture{ U"🍪"_emoji };

	// エフェクト
	Effect effectBackground;

	// 背景のクッキーが発生する間隔
	double cookieBackgroundSpawnTime = 0.1;

	// 背景のクッキーの蓄積時間
	double cookieBackgroundAccumulatedTime = 0.0;

	while (System::Update())
	{
		cookieBackgroundAccumulatedTime += Scene::DeltaTime();

		while (cookieBackgroundSpawnTime <= cookieBackgroundAccumulatedTime)
		{
			effectBackground.add<CookieBackgroundEffect>(RandomVec2(Rect{ 0, -150, 800, 100 }), texture);

			cookieBackgroundAccumulatedTime -= cookieBackgroundSpawnTime;
		}

		effectBackground.update();
	}
}
  • 描画順の都合上、クッキークリックの Effect とは別の Effect を作ります

# include <Siv3D.hpp>

/// @brief アイテムのボタン
/// @param rect ボタンの領域
/// @param texture ボタンの絵文字
/// @param font 文字描画に使うフォント
/// @param name アイテムの名前
/// @param desc アイテムの説明
/// @param count アイテムの所持数
/// @param enabled ボタンを押せるか
/// @return ボタンが押された場合 true, それ以外の場合は false
bool Button(const Rect& rect, const Texture& texture, const Font& font, const String& name, const String& desc, int32 count, bool enabled)
{
	if (enabled)
	{
		rect.draw(ColorF{ 0.3, 0.5, 0.9, 0.8 });

		rect.drawFrame(2, 2, ColorF{ 0.5, 0.7, 1.0 });

		if (rect.mouseOver())
		{
			Cursor::RequestStyle(CursorStyle::Hand);
		}
	}
	else
	{
		rect.draw(ColorF{ 0.0, 0.4 });

		rect.drawFrame(2, 2, ColorF{ 0.5 });
	}

	texture.scaled(0.5).drawAt(rect.x + 50, rect.y + 50);

	font(name).draw(30, rect.x + 100, rect.y + 15, Palette::White);

	font(desc).draw(18, rect.x + 102, rect.y + 60, Palette::White);

	font(count).draw(50, Arg::rightCenter((rect.x + rect.w - 20), (rect.y + 50)), Palette::White);

	return (enabled && rect.leftClicked());
}

// クッキーが降るエフェクト
struct CookieBackgroundEffect : IEffect
{
	// 初期座標
	Vec2 m_start;

	// 回転角度
	double m_angle;

	// テクスチャ
	Texture m_texture;

	CookieBackgroundEffect(const Vec2& start, const Texture& texture)
		: m_start{ start }
		, m_angle{ Random(2_pi) }
		, m_texture{ texture } {}

	bool update(double t) override
	{
		const Vec2 pos = m_start + 0.5 * t * t * Vec2{ 0, 120 };

		m_texture.scaled(0.3).rotated(m_angle).drawAt(pos, ColorF{ 1.0, (1.0 - t / 3.0) });

		return (t < 3.0);
	}
};

// クッキーが舞うエフェクト
struct CookieEffect : IEffect
{
	// 初期座標
	Vec2 m_start;

	// 初速
	Vec2 m_velocity;

	// 拡大倍率
	double m_scale;

	// 回転角度
	double m_angle;

	// テクスチャ
	Texture m_texture;

	CookieEffect(const Vec2& start, const Texture& texture)
		: m_start{ start }
		, m_velocity{ Circular{ 80, Random(-40_deg, 40_deg) } }
		, m_scale{ Random(0.2, 0.3) }
		, m_angle{ Random(2_pi) }
		, m_texture{ texture } {}

	bool update(double t) override
	{
		const Vec2 pos = m_start
			+ m_velocity * t + 0.5 * t * t * Vec2{ 0, 120 };

		m_texture.scaled(m_scale).rotated(m_angle).drawAt(pos, ColorF{ 1.0, (1.0 - t) });

		return (t < 1.0);
	}
};

// 「+1」が上昇するエフェクト
struct PlusOneEffect : IEffect
{
	// 初期座標
	Vec2 m_start;

	// フォント
	Font m_font;

	PlusOneEffect(const Vec2& start, const Font& font)
		: m_start{ start }
		, m_font{ font } {}

	bool update(double t) override
	{
		m_font(U"+1").drawAt(24, m_start.movedBy(0, t * -120), ColorF{ 1.0, (1.0 - t) });

		return (t < 1.0);
	}
};

void Main()
{
	// クッキーの絵文字
	Texture texture{ U"🍪"_emoji };

	// 農場の絵文字
	Texture farmEmoji{ U"🌾"_emoji };

	// 工場の絵文字
	Texture factoryEmoji{ U"🏭"_emoji };

	// フォント
	Font font{ FontMethod::MSDF, 48, Typeface::Bold };

	// エフェクト
	Effect effectBackground, effect;

	// クッキーのクリック円
	Circle cookieCircle{ 170, 300, 100 };

	// ばねの伸び
	double springX = 0.0;

	// ばねの速度
	double springVelocity = 0.0;

	// ばねの蓄積時間
	double springAccumulatedTime = 0.0;

	// クッキーの個数
	double cookies = 0;

	// 農場の所有数
	int32 farmCount = 0;

	// 工場の所有数
	int32 factoryCount = 0;

	// 農場の価格
	int32 farmCost = 10;

	// 工場の価格
	int32 factoryCost = 100;

	// クッキーの毎秒の生産量
	int32 cps = 0;

	// ゲームの経過時間の蓄積
	double accumulatedTime = 0.0;

	// 背景のクッキーが発生する間隔
	double cookieBackgroundSpawnTime = Math::Inf;

	// 背景のクッキーの蓄積時間
	double cookieBackgroundAccumulatedTime = 0.0;

	while (System::Update())
	{
		// クッキーの毎秒の生産量を計算する
		cps = (farmCount + factoryCount * 10);

		// ゲームの経過時間を加算する
		accumulatedTime += Scene::DeltaTime();

		// 0.1 秒以上蓄積していたら
		if (0.1 <= accumulatedTime)
		{
			accumulatedTime -= 0.1;

			// 0.1 秒分のクッキー生産を加算する
			cookies += (cps * 0.1);
		}

		// 背景のクッキー
		{
			// 背景のクッキーが発生する適当な間隔を cps から計算(多くなりすぎないよう緩やかに小さくなり、下限も設ける)
			cookieBackgroundSpawnTime = cps ? Max(1.0 / Math::Log2(cps * 2), 0.03) : Math::Inf;

			if (cps)
			{
				cookieBackgroundAccumulatedTime += Scene::DeltaTime();
			}

			while (cookieBackgroundSpawnTime <= cookieBackgroundAccumulatedTime)
			{
				effectBackground.add<CookieBackgroundEffect>(RandomVec2(Rect{ 0, -150, 800, 100 }), texture);

				cookieBackgroundAccumulatedTime -= cookieBackgroundSpawnTime;
			}
		}

		// ばねの蓄積時間を加算する
		springAccumulatedTime += Scene::DeltaTime();

		while (0.005 <= springAccumulatedTime)
		{
			// ばねの力(変化を打ち消す方向)
			double force = (-0.02 * springX);

			// 画面を押しているときに働く力
			if (cookieCircle.leftPressed())
			{
				force += 0.004;
			}

			// 速度に力を適用(減衰もさせる)
			springVelocity = (springVelocity + force) * 0.92;

			// 位置に反映
			springX += springVelocity;

			springAccumulatedTime -= 0.005;
		}

		// 農場の価格を計算する
		farmCost = 10 + (farmCount * 10);

		// 工場の価格を計算する
		factoryCost = 100 + (factoryCount * 100);

		// クッキー円上にマウスカーソルがあれば
		if (cookieCircle.mouseOver())
		{
			Cursor::RequestStyle(CursorStyle::Hand);
		}

		// クッキー円が左クリックされたら
		if (cookieCircle.leftClicked())
		{
			++cookies;

			// クッキーが舞うエフェクトを追加する
			effect.add<CookieEffect>(Cursor::Pos().movedBy(Random(-5, 5), Random(-5, 5)), texture);

			// 「+1」が上昇するエフェクトを追加する
			effect.add<PlusOneEffect>(Cursor::Pos().movedBy(Random(-5, 5), Random(-15, -5)), font);

			// 背景のクッキーを追加する
			effectBackground.add<CookieBackgroundEffect>(RandomVec2(Rect{ 0, -150, 800, 100 }), texture);
		}

		// 背景を描く
		Rect{ 0, 0, 800, 600 }.draw(Arg::top = ColorF{ 0.6, 0.5, 0.3 }, Arg::bottom = ColorF{ 0.2, 0.5, 0.3 });

		// 背景で降り注ぐクッキーを描画する
		effectBackground.update();

		// クッキーの後光を描く
		{
			for (int32 i = 0; i < 4; ++i)
			{
				double startAngle = Scene::Time() * 15_deg + i * 90_deg;
				Circle{ cookieCircle.center, 180 }.drawPie(startAngle, 60_deg, ColorF{ 1.0, 0.3 }, ColorF{ 1.0, 0.0 });
			}

			for (int32 i = 0; i < 6; ++i)
			{
				double startAngle = Scene::Time() * -15_deg + i * 60_deg;
				Circle{ cookieCircle.center, 180 }.drawPie(startAngle, 40_deg, ColorF{ 1.0, 0.3 }, ColorF{ 1.0, 0.0 });
			}
		}

		// クッキーの数を整数で表示する
		font(ThousandsSeparate((int32)cookies)).drawAt(60, 170, 100);

		// クッキーの生産量を表示する
		font(U"毎秒: {}"_fmt(cps)).drawAt(24, 170, 160);

		// クッキーを描画する
		texture.scaled(1.5 - springX).drawAt(cookieCircle.x, cookieCircle.y);

		// エフェクトを描画する
		effect.update();

		// 農場ボタン
		if (Button(Rect{ 340, 40, 420, 100 }, farmEmoji, font, U"クッキー農場", U"C{} / 1 CPS"_fmt(farmCost), farmCount, (farmCost <= cookies)))
		{
			cookies -= farmCost;
			++farmCount;
		}

		// 工場ボタン
		if (Button(Rect{ 340, 160, 420, 100 }, factoryEmoji, font, U"クッキー工場", U"C{} / 10 CPS"_fmt(factoryCost), factoryCount, (factoryCost <= cookies)))
		{
			cookies -= factoryCost;
			++factoryCount;
		}
	}
}

1.6 各種データや処理をクラスや関数にまとめる

  • 追加の処理を実装しやすくするため、各種データをクラスや関数でまとめます

# include <Siv3D.hpp>

/// @brief アイテムのボタン
/// @param rect ボタンの領域
/// @param texture ボタンの絵文字
/// @param font 文字描画に使うフォント
/// @param name アイテムの名前
/// @param desc アイテムの説明
/// @param count アイテムの所持数
/// @param enabled ボタンを押せるか
/// @return ボタンが押された場合 true, それ以外の場合は false
bool Button(const Rect& rect, const Texture& texture, const Font& font, const String& name, const String& desc, int32 count, bool enabled)
{
	if (enabled)
	{
		rect.draw(ColorF{ 0.3, 0.5, 0.9, 0.8 });

		rect.drawFrame(2, 2, ColorF{ 0.5, 0.7, 1.0 });

		if (rect.mouseOver())
		{
			Cursor::RequestStyle(CursorStyle::Hand);
		}
	}
	else
	{
		rect.draw(ColorF{ 0.0, 0.4 });

		rect.drawFrame(2, 2, ColorF{ 0.5 });
	}

	texture.scaled(0.5).drawAt(rect.x + 50, rect.y + 50);

	font(name).draw(30, rect.x + 100, rect.y + 15, Palette::White);

	font(desc).draw(18, rect.x + 102, rect.y + 60, Palette::White);

	font(count).draw(50, Arg::rightCenter((rect.x + rect.w - 20), (rect.y + 50)), Palette::White);

	return (enabled && rect.leftClicked());
}

// クッキーが降るエフェクト
struct CookieBackgroundEffect : IEffect
{
	// 初期座標
	Vec2 m_start;

	// 回転角度
	double m_angle;

	// テクスチャ
	Texture m_texture;

	CookieBackgroundEffect(const Vec2& start, const Texture& texture)
		: m_start{ start }
		, m_angle{ Random(2_pi) }
		, m_texture{ texture } {}

	bool update(double t) override
	{
		const Vec2 pos = m_start + 0.5 * t * t * Vec2{ 0, 120 };

		m_texture.scaled(0.3).rotated(m_angle).drawAt(pos, ColorF{ 1.0, (1.0 - t / 3.0) });

		return (t < 3.0);
	}
};

// クッキーが舞うエフェクト
struct CookieEffect : IEffect
{
	// 初期座標
	Vec2 m_start;

	// 初速
	Vec2 m_velocity;

	// 拡大倍率
	double m_scale;

	// 回転角度
	double m_angle;

	// テクスチャ
	Texture m_texture;

	CookieEffect(const Vec2& start, const Texture& texture)
		: m_start{ start }
		, m_velocity{ Circular{ 80, Random(-40_deg, 40_deg) } }
		, m_scale{ Random(0.2, 0.3) }
		, m_angle{ Random(2_pi) }
		, m_texture{ texture } {}

	bool update(double t) override
	{
		const Vec2 pos = m_start
			+ m_velocity * t + 0.5 * t * t * Vec2{ 0, 120 };

		m_texture.scaled(m_scale).rotated(m_angle).drawAt(pos, ColorF{ 1.0, (1.0 - t) });

		return (t < 1.0);
	}
};

// 「+1」が上昇するエフェクト
struct PlusOneEffect : IEffect
{
	// 初期座標
	Vec2 m_start;

	// フォント
	Font m_font;

	PlusOneEffect(const Vec2& start, const Font& font)
		: m_start{ start }
		, m_font{ font } {}

	bool update(double t) override
	{
		m_font(U"+1").drawAt(24, m_start.movedBy(0, t * -120), ColorF{ 1.0, (1.0 - t) });

		return (t < 1.0);
	}
};

// アイテムのデータ
struct Item
{
	// アイテムの絵文字
	Texture emoji;

	// アイテムの名前
	String name;

	// アイテムを初めて購入するときのコスト
	int32 initialCost;

	// アイテムの CPS
	int32 cps;

	// アイテムを count 個持っているときの購入コストを返す
	int32 getCost(int32 count) const
	{
		return initialCost * (count + 1);
	}
};

// クッキーのばね
class CookieSpring
{
public:

	void update(double deltaTime, bool pressed)
	{
		// ばねの蓄積時間を加算する
		m_accumulatedTime += deltaTime;

		while (0.005 <= m_accumulatedTime)
		{
			// ばねの力(変化を打ち消す方向)
			double force = (-0.02 * m_x);

			// 画面を押しているときに働く力
			if (pressed)
			{
				force += 0.004;
			}

			// 速度に力を適用(減衰もさせる)
			m_velocity = (m_velocity + force) * 0.92;

			// 位置に反映
			m_x += m_velocity;

			m_accumulatedTime -= 0.005;
		}
	}

	double get() const
	{
		return m_x;
	}

private:

	// ばねの伸び
	double m_x = 0.0;

	// ばねの速度
	double m_velocity = 0.0;

	// ばねの蓄積時間
	double m_accumulatedTime = 0.0;
};

// クッキーの後光を描く関数
void DrawHalo(const Vec2& center)
{
	for (int32 i = 0; i < 4; ++i)
	{
		double startAngle = Scene::Time() * 15_deg + i * 90_deg;
		Circle{ center, 180 }.drawPie(startAngle, 60_deg, ColorF{ 1.0, 0.3 }, ColorF{ 1.0, 0.0 });
	}

	for (int32 i = 0; i < 6; ++i)
	{
		double startAngle = Scene::Time() * -15_deg + i * 60_deg;
		Circle{ center, 180 }.drawPie(startAngle, 40_deg, ColorF{ 1.0, 0.3 }, ColorF{ 1.0, 0.0 });
	}
}

// アイテムの所有数をもとに CPS を計算する関数
int32 CalculateCPS(const Array<Item>& ItemTable, const Array<int32>& itemCounts)
{
	int32 cps = 0;

	for (size_t i = 0; i < ItemTable.size(); ++i)
	{
		cps += ItemTable[i].cps * itemCounts[i];
	}

	return cps;
}

void Main()
{
	// クッキーの絵文字
	const Texture texture{ U"🍪"_emoji };

	// アイテムのデータ
	const Array<Item> ItemTable = {
		{ Texture{ U"🌾"_emoji }, U"クッキー農場", 10, 1 },
		{ Texture{ U"🏭"_emoji }, U"クッキー工場", 100, 10 },
		{ Texture{ U"⚓"_emoji }, U"クッキー港", 1000, 100 },
	};

	// 各アイテムの所有数
	Array<int32> itemCounts(ItemTable.size()); // = { 0, 0, 0 }

	// フォント
	const Font font{ FontMethod::MSDF, 48, Typeface::Bold };

	// クッキーのクリック円
	const Circle CookieCircle{ 170, 300, 100 };

	// エフェクト
	Effect effectBackground, effect;

	// クッキーのばね
	CookieSpring cookieSpring;

	// クッキーの個数
	double cookies = 0;

	// ゲームの経過時間の蓄積
	double accumulatedTime = 0.0;

	// 背景のクッキーの蓄積時間
	double cookieBackgroundAccumulatedTime = 0.0;

	while (System::Update())
	{
		// クッキーの毎秒の生産量を計算する
		const int32 cps = CalculateCPS(ItemTable, itemCounts);

		// ゲームの経過時間を加算する
		accumulatedTime += Scene::DeltaTime();

		// 0.1 秒以上蓄積していたら
		if (0.1 <= accumulatedTime)
		{
			accumulatedTime -= 0.1;

			// 0.1 秒分のクッキー生産を加算する
			cookies += (cps * 0.1);
		}

		// 背景のクッキー
		{
			// 背景のクッキーが発生する適当な間隔を cps から計算(多くなりすぎないよう緩やかに小さくなり、下限も設ける)
			const double cookieBackgroundSpawnTime = cps ? Max(1.0 / Math::Log2(cps * 2), 0.03) : Math::Inf;

			if (cps)
			{
				cookieBackgroundAccumulatedTime += Scene::DeltaTime();
			}

			while (cookieBackgroundSpawnTime <= cookieBackgroundAccumulatedTime)
			{
				effectBackground.add<CookieBackgroundEffect>(RandomVec2(Rect{ 0, -150, 800, 100 }), texture);

				cookieBackgroundAccumulatedTime -= cookieBackgroundSpawnTime;
			}
		}

		// クッキーのばねを更新する
		cookieSpring.update(Scene::DeltaTime(), CookieCircle.leftPressed());

		// クッキー円上にマウスカーソルがあれば
		if (CookieCircle.mouseOver())
		{
			Cursor::RequestStyle(CursorStyle::Hand);
		}

		// クッキー円が左クリックされたら
		if (CookieCircle.leftClicked())
		{
			++cookies;

			// クッキーが舞うエフェクトを追加する
			effect.add<CookieEffect>(Cursor::Pos().movedBy(Random(-5, 5), Random(-5, 5)), texture);

			// 「+1」が上昇するエフェクトを追加する
			effect.add<PlusOneEffect>(Cursor::Pos().movedBy(Random(-5, 5), Random(-15, -5)), font);

			// 背景のクッキーを追加する
			effectBackground.add<CookieBackgroundEffect>(RandomVec2(Rect{ 0, -150, 800, 100 }), texture);
		}

		// 背景を描く
		Rect{ 0, 0, 800, 600 }.draw(Arg::top = ColorF{ 0.6, 0.5, 0.3 }, Arg::bottom = ColorF{ 0.2, 0.5, 0.3 });

		// 背景で降り注ぐクッキーを描画する
		effectBackground.update();

		// クッキーの後光を描く
		DrawHalo(CookieCircle.center);

		// クッキーの数を整数で表示する
		font(ThousandsSeparate((int32)cookies)).drawAt(60, 170, 100);

		// クッキーの生産量を表示する
		font(U"毎秒: {}"_fmt(cps)).drawAt(24, 170, 160);

		// クッキーを描画する
		texture.scaled(1.5 - cookieSpring.get()).drawAt(CookieCircle.center);

		// エフェクトを描画する
		effect.update();

		for (size_t i = 0; i < ItemTable.size(); ++i)
		{
			// アイテムの所有数
			const int32 itemCount = itemCounts[i];

			// アイテムの現在の価格
			const int32 itemCost = ItemTable[i].getCost(itemCount);

			// アイテム 1 つあたりの CPS
			const int32 itemCps = ItemTable[i].cps;

			// ボタン
			if (Button(Rect{ 340, (40 + 120 * i), 420, 100 }, ItemTable[i].emoji,
				font, ItemTable[i].name, U"C{} / {} CPS"_fmt(itemCost, itemCps), itemCount, (itemCost <= cookies)))
			{
				cookies -= itemCost;
				++itemCounts[i];
			}
		}
	}
}

1.7 ゲームをセーブする

  • シリアライズ機能を使うと簡単にセーブデータの読み書きができます
# include <Siv3D.hpp>

struct SaveData
{
	double cookies;

	Array<int32> itemCounts;

	// シリアライズに対応させるためのメンバ関数を定義する
	template <class Archive>
	void SIV3D_SERIALIZE(Archive& archive)
	{
		archive(cookies, itemCounts);
	}
};

void Main()
{
	{
		const SaveData saveData{ 1000, { 3, 2, 1 } };

		// バイナリファイルをオープン
		Serializer<BinaryWriter> writer{ U"test.save" };

		if (not writer) // もしオープンに失敗したら
		{
			throw Error{ U"Failed to open `test.save`" };
		}

		// シリアライズに対応したデータを記録
		writer(saveData);
	}

	// 読み込み先のデータ
	SaveData saveData;
	{
		// バイナリファイルをオープン
		Deserializer<BinaryReader> reader{ U"test.save" };

		if (reader) // もしオープンに成功したら
		{
			reader(saveData);
		}
	}

	Print << saveData.cookies;

	Print << saveData.itemCounts;

	while (System::Update())
	{

	}
}
  • ゲームの開始時にセーブデータの読み込みを、終了時に書き込みを行います
# include <Siv3D.hpp>

//ゲームのセーブデータ
struct SaveData
{
	double cookies;

	Array<int32> itemCounts;

	// シリアライズに対応させるためのメンバ関数を定義する
	template <class Archive>
	void SIV3D_SERIALIZE(Archive& archive)
	{
		archive(cookies, itemCounts);
	}
};

/// @brief アイテムのボタン
/// @param rect ボタンの領域
/// @param texture ボタンの絵文字
/// @param font 文字描画に使うフォント
/// @param name アイテムの名前
/// @param desc アイテムの説明
/// @param count アイテムの所持数
/// @param enabled ボタンを押せるか
/// @return ボタンが押された場合 true, それ以外の場合は false
bool Button(const Rect& rect, const Texture& texture, const Font& font, const String& name, const String& desc, int32 count, bool enabled)
{
	if (enabled)
	{
		rect.draw(ColorF{ 0.3, 0.5, 0.9, 0.8 });

		rect.drawFrame(2, 2, ColorF{ 0.5, 0.7, 1.0 });

		if (rect.mouseOver())
		{
			Cursor::RequestStyle(CursorStyle::Hand);
		}
	}
	else
	{
		rect.draw(ColorF{ 0.0, 0.4 });

		rect.drawFrame(2, 2, ColorF{ 0.5 });
	}

	texture.scaled(0.5).drawAt(rect.x + 50, rect.y + 50);

	font(name).draw(30, rect.x + 100, rect.y + 15, Palette::White);

	font(desc).draw(18, rect.x + 102, rect.y + 60, Palette::White);

	font(count).draw(50, Arg::rightCenter((rect.x + rect.w - 20), (rect.y + 50)), Palette::White);

	return (enabled && rect.leftClicked());
}

// クッキーが降るエフェクト
struct CookieBackgroundEffect : IEffect
{
	// 初期座標
	Vec2 m_start;

	// 回転角度
	double m_angle;

	// テクスチャ
	Texture m_texture;

	CookieBackgroundEffect(const Vec2& start, const Texture& texture)
		: m_start{ start }
		, m_angle{ Random(2_pi) }
		, m_texture{ texture } {}

	bool update(double t) override
	{
		const Vec2 pos = m_start + 0.5 * t * t * Vec2{ 0, 120 };

		m_texture.scaled(0.3).rotated(m_angle).drawAt(pos, ColorF{ 1.0, (1.0 - t / 3.0) });

		return (t < 3.0);
	}
};

// クッキーが舞うエフェクト
struct CookieEffect : IEffect
{
	// 初期座標
	Vec2 m_start;

	// 初速
	Vec2 m_velocity;

	// 拡大倍率
	double m_scale;

	// 回転角度
	double m_angle;

	// テクスチャ
	Texture m_texture;

	CookieEffect(const Vec2& start, const Texture& texture)
		: m_start{ start }
		, m_velocity{ Circular{ 80, Random(-40_deg, 40_deg) } }
		, m_scale{ Random(0.2, 0.3) }
		, m_angle{ Random(2_pi) }
		, m_texture{ texture } {}

	bool update(double t) override
	{
		const Vec2 pos = m_start
			+ m_velocity * t + 0.5 * t * t * Vec2{ 0, 120 };

		m_texture.scaled(m_scale).rotated(m_angle).drawAt(pos, ColorF{ 1.0, (1.0 - t) });

		return (t < 1.0);
	}
};

// 「+1」が上昇するエフェクト
struct PlusOneEffect : IEffect
{
	// 初期座標
	Vec2 m_start;

	// フォント
	Font m_font;

	PlusOneEffect(const Vec2& start, const Font& font)
		: m_start{ start }
		, m_font{ font } {}

	bool update(double t) override
	{
		m_font(U"+1").drawAt(24, m_start.movedBy(0, t * -120), ColorF{ 1.0, (1.0 - t) });

		return (t < 1.0);
	}
};

// アイテムのデータ
struct Item
{
	// アイテムの絵文字
	Texture emoji;

	// アイテムの名前
	String name;

	// アイテムを初めて購入するときのコスト
	int32 initialCost;

	// アイテムの CPS
	int32 cps;

	// アイテムを count 個持っているときの購入コストを返す
	int32 getCost(int32 count) const
	{
		return initialCost * (count + 1);
	}
};

// クッキーのばね
class CookieSpring
{
public:

	void update(double deltaTime, bool pressed)
	{
		// ばねの蓄積時間を加算する
		m_accumulatedTime += deltaTime;

		while (0.005 <= m_accumulatedTime)
		{
			// ばねの力(変化を打ち消す方向)
			double force = (-0.02 * m_x);

			// 画面を押しているときに働く力
			if (pressed)
			{
				force += 0.004;
			}

			// 速度に力を適用(減衰もさせる)
			m_velocity = (m_velocity + force) * 0.92;

			// 位置に反映
			m_x += m_velocity;

			m_accumulatedTime -= 0.005;
		}
	}

	double get() const
	{
		return m_x;
	}

private:

	// ばねの伸び
	double m_x = 0.0;

	// ばねの速度
	double m_velocity = 0.0;

	// ばねの蓄積時間
	double m_accumulatedTime = 0.0;
};

// クッキーの後光を描く関数
void DrawHalo(const Vec2& center)
{
	for (int32 i = 0; i < 4; ++i)
	{
		double startAngle = Scene::Time() * 15_deg + i * 90_deg;
		Circle{ center, 180 }.drawPie(startAngle, 60_deg, ColorF{ 1.0, 0.3 }, ColorF{ 1.0, 0.0 });
	}

	for (int32 i = 0; i < 6; ++i)
	{
		double startAngle = Scene::Time() * -15_deg + i * 60_deg;
		Circle{ center, 180 }.drawPie(startAngle, 40_deg, ColorF{ 1.0, 0.3 }, ColorF{ 1.0, 0.0 });
	}
}

// アイテムの所有数をもとに CPS を計算する関数
int32 CalculateCPS(const Array<Item>& ItemTable, const Array<int32>& itemCounts)
{
	int32 cps = 0;

	for (size_t i = 0; i < ItemTable.size(); ++i)
	{
		cps += ItemTable[i].cps * itemCounts[i];
	}

	return cps;
}

void Main()
{
	// クッキーの絵文字
	const Texture texture{ U"🍪"_emoji };

	// アイテムのデータ
	const Array<Item> ItemTable = {
		{ Texture{ U"🌾"_emoji }, U"クッキー農場", 10, 1 },
		{ Texture{ U"🏭"_emoji }, U"クッキー工場", 100, 10 },
		{ Texture{ U"⚓"_emoji }, U"クッキー港", 1000, 100 },
	};

	// 各アイテムの所有数
	Array<int32> itemCounts(ItemTable.size()); // = { 0, 0, 0 }

	// フォント
	const Font font{ FontMethod::MSDF, 48, Typeface::Bold };

	// クッキーのクリック円
	const Circle CookieCircle{ 170, 300, 100 };

	// エフェクト
	Effect effectBackground, effect;

	// クッキーのばね
	CookieSpring cookieSpring;

	// クッキーの個数
	double cookies = 0;

	// ゲームの経過時間の蓄積
	double accumulatedTime = 0.0;

	// 背景のクッキーの蓄積時間
	double cookieBackgroundAccumulatedTime = 0.0;

	// セーブデータが見つかればそれを読み込む
	{
		// バイナリファイルをオープン
		Deserializer<BinaryReader> reader{ U"game.save" };

		if (reader) // もしオープンに成功したら
		{
			SaveData saveData;

			reader(saveData);

			cookies = saveData.cookies;

			itemCounts = saveData.itemCounts;
		}
	}

	while (System::Update())
	{
		// クッキーの毎秒の生産量を計算する
		const int32 cps = CalculateCPS(ItemTable, itemCounts);

		// ゲームの経過時間を加算する
		accumulatedTime += Scene::DeltaTime();

		// 0.1 秒以上蓄積していたら
		if (0.1 <= accumulatedTime)
		{
			accumulatedTime -= 0.1;

			// 0.1 秒分のクッキー生産を加算する
			cookies += (cps * 0.1);
		}

		// 背景のクッキー
		{
			// 背景のクッキーが発生する適当な間隔を cps から計算(多くなりすぎないよう緩やかに小さくなり、下限も設ける)
			const double cookieBackgroundSpawnTime = cps ? Max(1.0 / Math::Log2(cps * 2), 0.03) : Math::Inf;

			if (cps)
			{
				cookieBackgroundAccumulatedTime += Scene::DeltaTime();
			}

			while (cookieBackgroundSpawnTime <= cookieBackgroundAccumulatedTime)
			{
				effectBackground.add<CookieBackgroundEffect>(RandomVec2(Rect{ 0, -150, 800, 100 }), texture);

				cookieBackgroundAccumulatedTime -= cookieBackgroundSpawnTime;
			}
		}

		// クッキーのばねを更新する
		cookieSpring.update(Scene::DeltaTime(), CookieCircle.leftPressed());

		// クッキー円上にマウスカーソルがあれば
		if (CookieCircle.mouseOver())
		{
			Cursor::RequestStyle(CursorStyle::Hand);
		}

		// クッキー円が左クリックされたら
		if (CookieCircle.leftClicked())
		{
			++cookies;

			// クッキーが舞うエフェクトを追加する
			effect.add<CookieEffect>(Cursor::Pos().movedBy(Random(-5, 5), Random(-5, 5)), texture);

			// 「+1」が上昇するエフェクトを追加する
			effect.add<PlusOneEffect>(Cursor::Pos().movedBy(Random(-5, 5), Random(-15, -5)), font);

			// 背景のクッキーを追加する
			effectBackground.add<CookieBackgroundEffect>(RandomVec2(Rect{ 0, -150, 800, 100 }), texture);
		}

		// 背景を描く
		Rect{ 0, 0, 800, 600 }.draw(Arg::top = ColorF{ 0.6, 0.5, 0.3 }, Arg::bottom = ColorF{ 0.2, 0.5, 0.3 });

		// 背景で降り注ぐクッキーを描画する
		effectBackground.update();

		// クッキーの後光を描く
		DrawHalo(CookieCircle.center);

		// クッキーの数を整数で表示する
		font(ThousandsSeparate((int32)cookies)).drawAt(60, 170, 100);

		// クッキーの生産量を表示する
		font(U"毎秒: {}"_fmt(cps)).drawAt(24, 170, 160);

		// クッキーを描画する
		texture.scaled(1.5 - cookieSpring.get()).drawAt(CookieCircle.center);

		// エフェクトを描画する
		effect.update();

		for (size_t i = 0; i < ItemTable.size(); ++i)
		{
			// アイテムの所有数
			const int32 itemCount = itemCounts[i];

			// アイテムの現在の価格
			const int32 itemCost = ItemTable[i].getCost(itemCount);

			// アイテム 1 つあたりの CPS
			const int32 itemCps = ItemTable[i].cps;

			// ボタン
			if (Button(Rect{ 340, (40 + 120 * i), 420, 100 }, ItemTable[i].emoji,
				font, ItemTable[i].name, U"C{} / {} CPS"_fmt(itemCost, itemCps), itemCount, (itemCost <= cookies)))
			{
				cookies -= itemCost;
				++itemCounts[i];
			}
		}
	}

	// メインループの後、終了時にゲームをセーブ
	{
		// バイナリファイルをオープン
		Serializer<BinaryWriter> writer{ U"game.save" };

		// シリアライズに対応したデータを記録
		writer(SaveData{ cookies, itemCounts });
	}
}