Skip to content

36. インタラクションの実装

ここまで学んだことを使って、様々なインタラクティブ要素を実装します。

36.1 クリックした場所に円を配置する

# include <Siv3D.hpp>

void Main()
{
	Scene::SetBackground(ColorF{ 0.8, 0.9, 1.0 });

	Array<Circle> circles;

	while (System::Update())
	{
		if (MouseL.down())
		{
			// クリックした位置に半径 10 ~ 30 の円を追加する
			circles << Circle{ Cursor::Pos(), Random(10.0, 30.0) };
		}

		for (const auto& circle : circles)
		{
			// x 座標に応じて色を変える
			circle.draw(HSV{ circle.center.x, 0.8, 0.9 });
		}
	}
}

36.2 グリッドのマスに色を塗る

# include <Siv3D.hpp>

void Main()
{
	Scene::SetBackground(ColorF{ 0.8, 0.9, 1.0 });

	// 8x6 の二次元配列を作成し、全ての要素を 0 で初期化する
	Grid<int32> grid(8, 6);

	while (System::Update())
	{
		for (int32 y = 0; y < grid.height(); ++y)
		{
			for (int32 x = 0; x < grid.width(); ++x)
			{
				const RectF rect{ (x * 100), (y * 100), 100 };

				if (rect.leftClicked())
				{
                    // クリックのたびに要素を 0 → 1 → 2 → 3 → 0 → 1 → ... と変化させる
					++grid[y][x] %= 4;
				}

				const ColorF color{ (3 - grid[y][x]) / 3.0 };

				rect.stretched(-1).draw(color);
			}
		}
	}
}

36.3 バウンドする複数のボール

# include <Siv3D.hpp>

struct Ball
{
	Vec2 pos;

	Vec2 velocity;
};

void Main()
{
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });

	const double ballRadius = 20.0;

	Array<Ball> balls;

	for (int32 i = 0; i < 5; ++i)
	{
		balls << Ball{ RandomVec2(Scene::Rect().stretched(-ballRadius)), RandomVec2(200) };
	}

	while (System::Update())
	{
		for (auto& ball : balls)
		{
			ball.pos += (ball.velocity * Scene::DeltaTime());

			if ((ball.pos.x <= ballRadius) || (Scene::Width() <= (ball.pos.x + ballRadius)))
			{
				ball.velocity.x *= -1.0;
			}

			if ((ball.pos.y <= ballRadius) || (Scene::Height() <= (ball.pos.y + ballRadius)))
			{
				ball.velocity.y *= -1.0;
			}
		}

		for (const auto& ball : balls)
		{
			Circle{ ball.pos, ballRadius }.draw();
		}
	}
}

36.4 抽選

# include <Siv3D.hpp>

void Main()
{
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });

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

	const Array<String> options = { U"New York", U"London", U"Paris", U"Tokyo", U"Sydney", U"Berlin" };

	const Rect optionRect{ Arg::center = Scene::Center().movedBy(0, -60), 400, 100 };

	String result;

	while (System::Update())
	{
		optionRect.draw();

		if (result)
		{
			font(result).drawAt(60, optionRect.center(), ColorF{ 0.11 });

			if (SimpleGUI::Button(U"Start", Scene::Center().movedBy(-60, 40), 120))
			{
				result.clear();
			}
		}
		else
		{
			font(options.choice()).drawAt(60, optionRect.center(), ColorF{ 0.11 });

			if (SimpleGUI::Button(U"Stop", Scene::Center().movedBy(-60, 40), 120))
			{
				result = options.choice();
			}
		}
	}
}

36.5 弾幕を発射する

# include <Siv3D.hpp>

struct Bullet
{
	// 位置
	Vec2 pos;

	// 速度
	Vec2 velocity;
};

void Main()
{
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });

	const Texture textureEnemy{ U"🛸"_emoji };

	// 発射周期(秒)
	const double fireInterval = 0.08;

	// 蓄積時間(秒)
	double accumulatedTime = 0.0;

	// 発射方向
	double angle = 0.0;

	Array<Bullet> bullets;

	while (System::Update())
	{
		ClearPrint();
		Print << bullets.size();

		accumulatedTime += Scene::DeltaTime();

		// 敵の位置
		const Vec2 enemyPos{ (400 + Periodic::Sine1_1(4s) * 200.0), 200 };

		// 蓄積時間が周期を超えたら
		if (fireInterval <= accumulatedTime)
		{
			const Vec2 velocity = Circular{ 120, angle };

			bullets << Bullet{ enemyPos, velocity };

			angle += 15_deg;

			accumulatedTime -= fireInterval;
		}

		// 弾の移動
		for (auto& bullet : bullets)
		{
			bullet.pos += (bullet.velocity * Scene::DeltaTime());
		}

		// 画面外に出た弾を削除
		bullets.remove_if([](const Bullet& bullet) { return (not bullet.pos.intersects(Scene::Rect())); });

		textureEnemy.drawAt(enemyPos);

		for (const auto& bullet : bullets)
		{
			Circle{ bullet.pos, 8 }.draw();
		}
	}
}

36.6 複数の絵文字をマウスで配置する

# include <Siv3D.hpp>

using ItemID = uint32;

struct Item
{
	// 更新時刻
	uint64 updateTime = 0;

	// 中心座標
	Vec2 pos = Vec2{ 0, 0 };

	// ID
	ItemID id = 0;

	// 絵文字の種類
	int32 type = 0;
};

void Main()
{
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });

	const Array<Texture> emojis =
	{
		Texture{ U"🍔"_emoji },
		Texture{ U"🍅"_emoji },
		Texture{ U"🥗"_emoji },
		Texture{ U"🍣"_emoji },
		Texture{ U"🍩"_emoji },
		Texture{ U"🍙"_emoji },
	};

	// ID 発行用のカウンター
	ItemID idCounter = 0;

	Array<Item> items;

	for (int32 i = 0; i < 12; ++i)
	{
		items << Item{ Time::GetMillisec(), RandomVec2(Scene::Rect().stretched(-50)), ++idCounter, Random(0, static_cast<int32>(emojis.size() - 1))};
	}

	// 選択されているアイテムの ID
	Optional<ItemID> selectedItemID;

	while (System::Update())
	{
		// マウスオーバーしているアイテムの ID
		Optional<ItemID> mouseOverItemID;

		if (MouseL.up())
		{
			selectedItemID.reset();
		}

		if (selectedItemID)
		{
			for (auto& item : items)
			{
				if (item.id == *selectedItemID)
				{
					item.pos.moveBy(Cursor::DeltaF());
					break;
				}
			}
		}
		else
		{
			for (auto& item : items)
			{
				if (Circle{ item.pos, 50 }.mouseOver())
				{
					Cursor::RequestStyle(CursorStyle::Hand);
					mouseOverItemID = item.id;

					if (MouseL.down())
					{
						item.updateTime = Time::GetMillisec();
						selectedItemID = item.id;
					}

					break;
				}
			}
		}

		// 更新時刻が新しい順に並び替える
		items.sort_by([](const Item& a, const Item& b) { return a.updateTime > b.updateTime; });

		// 更新時刻が古い順に描画する
		for (int32 i = (static_cast<int32>(items.size()) - 1); 0 <= i; --i)
		{
			const auto& item = items[i];

			// マウスオーバーしているか、選択されているアイテムは少し大きく描画する
			const bool mouseOver = ((item.id == mouseOverItemID) || (item.id == selectedItemID));
			emojis[item.type].scaled(mouseOver ? 1.1 : 1.0).drawAt(item.pos);
		}
	}
}

36.7 慣性のある移動

# include <Siv3D.hpp>

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

	const double speed = 300.0;

	Vec2 targetPos{ 400, 300 };

	Vec2 pos{ 400, 300 };

	Vec2 velocity{ 0, 0 };

	while (System::Update())
	{
		const double deltaTime = Scene::DeltaTime();

		if (KeyLeft.pressed())
		{
			targetPos.x -= (speed * deltaTime);
		}

		if (KeyRight.pressed())
		{
			targetPos.x += (speed * deltaTime);
		}

		if (KeyUp.pressed())
		{
			targetPos.y -= (speed * deltaTime);
		}

		if (KeyDown.pressed())
		{
			targetPos.y += (speed * deltaTime);
		}

		pos = Math::SmoothDamp(pos, targetPos, velocity, 0.3);

		RectF{ Arg::center = pos, 120, 80 }.draw(ColorF{ 0.2, 0.6, 0.9 });
	}
}

36.8 ゲームのメッセージボックス

# include <Siv3D.hpp>

void Main()
{
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });

	const Font font{ FontMethod::MSDF, 48, Typeface::Medium };

	// メッセージの配列
	const Array<String> messages =
	{
		U"Twinkle, twinkle, little star,\nHow I wonder what you are!",
		U"Up above the world so high,\nLike a diamond in the sky.",
		U"When the blazing sun is gone,\nWhen he nothing shines upon,",
		U"Then you show your little light,\nTwinkle, twinkle, all the night.",
	};

	// メッセージボックスの位置とサイズ
	const Rect messageBox{ 40, 440, 720, 120 };

	// メッセージのインデックス
	size_t messageIndex = 0;

	// メッセージの表示を開始してからの経過時間を測定するストップウォッチ
	Stopwatch stopwatch{ StartImmediately::Yes };

	while (System::Update())
	{
		// 表示する文字数
		int32 count = Max(((stopwatch.ms() - 200) / 24), 0);

		// 現在のメッセージがすべて表示されているか
		const bool finished = (static_cast<int32>(messages[messageIndex].length()) <= count);

		if (finished && (messageBox.leftClicked() || KeySpace.down()))
		{
			// 次のメッセージに切り替える
			++messageIndex %= messages.size();
			stopwatch.restart();
			count = 0;
		}

		// メッセージボックスを描画する
		messageBox.rounded(10).drawShadow(Vec2{ 1, 1 }, 8).draw().drawFrame(2, ColorF{ 0.4 });

		// メッセージを描画する
		font(messages[messageIndex].substr(0, count)).draw(28, messageBox.stretched(-36, -20), ColorF{ 0.11 });

		if (messageBox.mouseOver())
		{
			Cursor::RequestStyle(CursorStyle::Hand);
		}

		if (finished)
		{
			// メッセージの表示が終わっていたら、▼ を描画する
			Triangle{ messageBox.br().movedBy(-30, -30), 20, 180_deg }.draw(ColorF{ 0.11, Periodic::Sine0_1(2.0s) });
		}
	}
}

36.9 メッセージログ

# include <Siv3D.hpp>

void Main()
{
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });

	TextEditState textEditState;

	ListBoxState listBoxState;

	while (System::Update())
	{
		SimpleGUI::ListBox(listBoxState, Vec2{ 40, 40 }, 720, 220);

		bool enter = false;
		{
			const bool previous = textEditState.active;

			SimpleGUI::TextBox(textEditState, Vec2{ 40, 280 }, 600);

			// エンターキーが押されてテキストボックスが非アクティブになったか
			enter = (previous && (textEditState.active == false) && textEditState.enterKey);
		}

		if (SimpleGUI::Button(U"Submit", Vec2{ 660, 280 }, 100, (not textEditState.text.isEmpty()))
			|| enter)
		{
			// リストボックスにテキストを追加する
			listBoxState.items << textEditState.text;

			// 追加したテキストが見えるようにスクロール位置を最大にする(次の SimpleGUI::ListBox() で適切な値に補正される)
			listBoxState.scroll = Largest<int32>;

			// テキストボックスをクリアする
			textEditState.clear();

			// エンターキーが押されてテキストボックスが非アクティブになった場合、再びアクティブにする
			if (enter)
			{
				textEditState.active = true;
			}
		}
	}
}

36.10 徐々に変化する数値

# include <Siv3D.hpp>

void Main()
{
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });

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

	int32 targetValue = 0;

	double currentValue = 0.0;

	double velocity = 0.0;

	while (System::Update())
	{
		ClearPrint();
		Print << U"targetValue: " << targetValue;

		currentValue = Math::SmoothDamp(currentValue, targetValue, velocity, 0.3);

		const int32 value = static_cast<int32>(Math::Round(currentValue));

		font(value).draw(40, Arg::topRight(160, 90), ColorF{ 0.11 });

		if (SimpleGUI::Button(U"+1", Vec2{ 200, 100 }, 80))
		{
			++targetValue;
		}

		if (SimpleGUI::Button(U"-1", Vec2{ 300, 100 }, 80))
		{
			--targetValue;
		}

		if (SimpleGUI::Button(U"+10", Vec2{ 400, 100 }, 80))
		{
			targetValue += 10;
		}

		if (SimpleGUI::Button(U"-10", Vec2{ 500, 100 }, 80))
		{
			targetValue -= 10;
		}

		if (SimpleGUI::Button(U"+100", Vec2{ 600, 100 }, 80))
		{
			targetValue += 100;
		}

		if (SimpleGUI::Button(U"-100", Vec2{ 700, 100 }, 80))
		{
			targetValue -= 100;
		}
	}
}

36.11 リストの並び替え

# include <Siv3D.hpp>

struct Item
{
	int32 id = 0;

	String text;

	// アイテムの描画
	void draw(const Vec2& basePos, const Font& font, int32 order) const
	{
		drawImpl(getRect(basePos, order), font, false);
	}

	// 掴んでいるアイテムの描画
	void drawGrabbed(const Vec2& basePos, const Font& font, const Vec2& offset, int32 order) const
	{
		drawImpl(getRect(basePos, order).movedBy(Cursor::PosF() - offset), font, true);
	}

	// アイテムの長方形を返す
	RectF getRect(const Vec2& basePos, int32 order) const
	{
		return{ basePos.movedBy(0, (80 * order)), 400, 70 };
	}

private:

	void drawImpl(const RectF& rect, const Font& font, bool shadow) const
	{
		if (shadow)
		{
			rect.rounded(8).drawShadow(Vec2{ 2, 2 }, 16, 2).draw();
		}
		else
		{
			rect.rounded(8).draw();
		}

		font(text).draw(30, Arg::leftCenter = rect.leftCenter().movedBy(30, 0), ColorF{ 0.11 });
	}
};

// 掴んでいるアイテムの情報
struct GrabbedItem
{
	int32 id = 0;

	int32 oldOrder = 0;

	Vec2 offset{ 0, 0 };
};

void Main()
{
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });

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

	Array<Item> items =
	{
		{ 111, U"Apple" },
		{ 222, U"Bird" },
		{ 333, U"Cat" },
		{ 444, U"Dog" },
		{ 555, U"Elephant" },
	};

	const Point basePos{ 80, 80 };

	Optional<GrabbedItem> grabbedItem;

	while (System::Update())
	{
		// リストの背景を描画する
		RectF{ basePos, 400, 600 }.stretched(24).rounded(8).draw(ColorF{ 0.9 });

		// アイテムを掴む処理
		if (not grabbedItem)
		{
			// アイテムのリスト内順序
			int32 order = 0;

			for (auto& item : items)
			{
				// アイテムの長方形
				const RectF rect = item.getRect(basePos, order);

				if (rect.mouseOver())
				{
					Cursor::RequestStyle(CursorStyle::Hand);

					if (rect.leftClicked())
					{
						// アイテムを掴む
						grabbedItem = { item.id, order, Cursor::PosF() };
					}

					break;
				}

				++order;
			}
		}

		// 掴んでいるアイテムの真下のリスト内順序。アイテムを掴んでいない場合は -1
		int32 targetOrder = (grabbedItem ? Clamp(((Cursor::Pos().y - basePos.y) / 80), 0, (static_cast<int32>(items.size()) - 1)) : -1);

		// 掴んでいるアイテムを置く処理
		if (grabbedItem && MouseL.up())
		{
			// 以前と異なるリスト内順序の場合
			if (targetOrder != grabbedItem->oldOrder)
			{
				// アイテムを一旦コピー
				auto tmp = std::move(items[grabbedItem->oldOrder]);

				// 以前の場所にあったアイテムを削除する
				items.erase(items.begin() + grabbedItem->oldOrder);

				// 新しい場所にアイテムを挿入する
				items.insert((items.begin() + targetOrder), std::move(tmp));
			}

			// アイテムを掴んでいない状態にする
			grabbedItem.reset();
			targetOrder = -1;
		}

		if (grabbedItem)
		{
			Cursor::RequestStyle(CursorStyle::Hand);
		}

		// リスト上のアイテムを描画する
		{
			int32 order = 0;

			for (const auto& item : items)
			{
				if (targetOrder == order)
				{
					++order;
				}

				// その位置に掴んでいるアイテムがある場合は描画しない
				if (grabbedItem && (grabbedItem->id == item.id))
				{
					continue;
				}

				item.draw(basePos, font, order);

				++order;
			}
		}

		// 掴んでいるアイテムを描画する
		if (grabbedItem)
		{
			for (const auto& item : items)
			{
				if (grabbedItem->id == item.id)
				{
					item.drawGrabbed(basePos, font, grabbedItem->offset, grabbedItem->oldOrder);
					break;
				}
			}
		}
	}
}