コンテンツにスキップ

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

チュートリアル 3 ~ 39 の内容を使って、インタラクティブなプログラムを作成します。

40.1 クリックした場所への円の配置

# include <Siv3D.hpp>

void DrawCircles(const Array<Circle>& circles)
{
	for (const auto& circle : circles)
	{
		circle.draw(HSV{ circle.center.x, 0.8, 0.9 });
	}
}

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) };
		}

		DrawCircles(circles);
	}
}

40.2 グリッドのマスの色塗り

# include <Siv3D.hpp>

void UpdateGrid(Grid<int32>& grid)
{
	// クリックされていない場合は何もしない
	if (not MouseL.down())
	{
		return;
	}

	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.mouseOver())
			{
				// クリックのたびに要素を 0 → 1 → 2 → 3 → 0 → 1 → ... と変化させる
				++grid[y][x] %= 4;
			}
		}
	}
}

void DrawGrid(const Grid<int32>& grid)
{
	for (int32 y = 0; y < grid.height(); ++y)
	{
		for (int32 x = 0; x < grid.width(); ++x)
		{
			const RectF rect{ (x * 100), (y * 100), 100 };
			const ColorF color{ (3 - grid[y][x]) / 3.0 };
			rect.stretched(-1).draw(color);
		}
	}
}

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

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

	while (System::Update())
	{
		UpdateGrid(grid);

		DrawGrid(grid);
	}
}

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

# include <Siv3D.hpp>

struct Ball
{
	Vec2 pos;

	Vec2 velocity;
};

void UpdateBalls(Array<Ball>& balls, double ballRadius)
{
	const Size sceneSize{ 800, 600 };

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

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

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

void DrawBalls(const Array<Ball>& balls, double ballRadius)
{
	for (const auto& ball : balls)
	{
		Circle{ ball.pos, ballRadius }.draw().drawFrame(2, 0, ColorF{ 0.2 });
	}
}

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())
	{
		UpdateBalls(balls, ballRadius);

		DrawBalls(balls, ballRadius);
	}
}

40.4 抽選

# include <Siv3D.hpp>

void DrawBox(const Rect& rect, const Font& font, const String& text)
{
	rect.rounded(6).draw();

	rect.stretched(-3).rounded(3).drawFrame(2, ColorF{ 0.75 });

	font(text).drawAt(60, rect.center(), ColorF{ 0.2 });
}

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 rect{ Arg::center(400, 240), 400, 100 };

	// 抽選中は空の文字列
	String result;

	while (System::Update())
	{
		if (result)
		{
			DrawBox(rect, font, result);

			if (SimpleGUI::Button(U"Start", Vec2{ 340, 340 }, 120))
			{
				result.clear();
			}
		}
		else
		{
			DrawBox(rect, font, options.choice());

			if (SimpleGUI::Button(U"Stop", Vec2{ 340, 340 }, 120))
			{
				result = options.choice();
			}
		}
	}
}

40.5 弾幕の発射

# include <Siv3D.hpp>

struct Bullet
{
	// 位置
	Vec2 pos;

	// 速度
	Vec2 velocity;
};

// 敵の状態
struct EnemyState
{
	// 弾の発射周期(秒)
	double fireInterval = 0.08;

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

	// 弾の発射方向
	double bulletLaunchAngle = 0_deg;

	// 位置
	Vec2 pos;

	// 弾の配列
	Array<Bullet> bullets;

	void update(double deltaTime)
	{
		pos = Vec2{ (400 + Periodic::Sine1_1(4s) * 200.0), 200 };

		accumulatedTime += deltaTime;

		// 蓄積時間が周期を超えたら新しい弾を発射する
		if (fireInterval <= accumulatedTime)
		{
			const Vec2 velocity = Circular{ 120, bulletLaunchAngle };

			bullets << Bullet{ pos, velocity };

			bulletLaunchAngle += 15_deg;

			accumulatedTime -= fireInterval;
		}
	}
};

void UpdateBullets(Array<Bullet>& bullets, double deltaTime)
{
	// 弾を移動させる
	for (auto& bullet : bullets)
	{
		bullet.pos += (bullet.velocity * deltaTime);
	}

	// 画面外に出た弾を削除する
	const Rect sceneRect{ 800, 600 };
	bullets.remove_if([&](const Bullet& bullet) { return (not bullet.pos.intersects(sceneRect)); });
}

void DrawBullets(const Array<Bullet>& bullets)
{
	for (const auto& bullet : bullets)
	{
		Circle{ bullet.pos, 8 }.draw().drawFrame(2, 0, ColorF{ 0.2 });
	}
}

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

	const Texture textureEnemy{ U"🛸"_emoji };

	// 敵の状態
	EnemyState enemyState;

	while (System::Update())
	{
		/////////////////////////////////
		//
		//	更新
		//
		/////////////////////////////////

		const double deltaTime = Scene::DeltaTime();
		
		// 敵の状態を更新する
		enemyState.update(deltaTime);
		
		// 弾の状態を更新する
		UpdateBullets(enemyState.bullets, deltaTime);

		/////////////////////////////////
		//
		//	描画
		//
		/////////////////////////////////
	
		// 敵を描く
		textureEnemy.drawAt(enemyState.pos);
		
		// 弾を描く
		DrawBullets(enemyState.bullets);
	}
}

40.6 複数の絵文字のマウスでの配置

# include <Siv3D.hpp>

using ItemID = uint32;

struct Item
{
	// 更新時刻(最後にクリックした時刻)
	uint64 updateTime = 0;

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

	// ID
	ItemID id = 0;

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

Array<Item> GenerateItems()
{
	Array<Item> items;

	for (int32 i = 0; i < 12; ++i)
	{
		const uint64 updateTime = Time::GetMillisec();
		const Vec2 pos = RandomVec2(Scene::Rect().stretched(-50));
		const ItemID id = (i + 1);
		const int32 type = Random(0, 5);
		items << Item{ updateTime, pos, id, type };
	}

	return items;
}

void MoveItem(Array<Item>& items, ItemID selectedItemID)
{
	for (auto& item : items)
	{
		if (item.id == selectedItemID)
		{
			const Vec2 move = Cursor::DeltaF(); // 前フレームからのマウスの移動量
			item.pos.moveBy(move);
			return;
		}
	}
}

void SelectItem(Array<Item>& items,
	Optional<ItemID>& mouseOverItemID, Optional<ItemID>& selectedItemID)
{
	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;
			}

			return;
		}
	}
}

void SortByUpdateTime(Array<Item>& items)
{
	items.sort_by([](const Item& a, const Item& b) { return a.updateTime > b.updateTime; });
}

void DrawItems(const Array<Item>& items, const Array<Texture>& emojis,
	const Optional<ItemID>& mouseOverItemID, const Optional<ItemID>& selectedItemID)
{
	// 更新時刻が古い順に描画する
	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);
	}
}

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 },
	};

	Array<Item> items = GenerateItems();

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

	while (System::Update())
	{
		/////////////////////////////////
		//
		//	更新
		//
		/////////////////////////////////

		if (MouseL.up())
		{
			// アイテムの選択を解除する
			selectedItemID.reset();
		}

		// マウスオーバーしているアイテムの ID
		Optional<ItemID> mouseOverItemID;

		if (selectedItemID)
		{
			// 選択中のアイテムをマウスで移動させる
			MoveItem(items, *selectedItemID);
		}
		else
		{
			// アイテムを選択する
			SelectItem(items, mouseOverItemID, selectedItemID);
		}

		// 更新時刻が新しい順に並び替える
		SortByUpdateTime(items);

		/////////////////////////////////
		//
		//	描画
		//
		/////////////////////////////////

		// アイテムを描画する
		DrawItems(items, emojis, mouseOverItemID, selectedItemID);
	}
}

40.7 慣性のある移動

# include <Siv3D.hpp>

struct SmoothedVec2
{
	Vec2 current{ 400, 300 };

	Vec2 target = current;

	Vec2 velocity{ 0, 0 };

	void update()
	{
		current = Math::SmoothDamp(current, target, velocity, 0.3);
	}
};

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

	const double speed = 300.0;

	SmoothedVec2 pos;

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

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

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

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

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

		pos.update();

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

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

# include <Siv3D.hpp>

struct DialogBox
{
	Rect rect{ 40, 440, 720, 120 };

	// メッセージの配列
	Array<String> messages;

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

	// メッセージの表示時間を計測するストップウォッチ
	Stopwatch stopwatch;

	bool isFinished() const
	{
		// 表示する文字数
		const int32 count = Max(((stopwatch.ms() - 200) / 24), 0);

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

	void update()
	{
		if (isFinished() && (rect.leftClicked() || KeySpace.down()))
		{
			// 次のメッセージに切り替える
			++messageIndex %= messages.size();

			// ストップウォッチをリセットする
			stopwatch.restart();
		}
	}

	void draw(const Font& font) const
	{
		// 表示する文字数
		const int32 count = Max(((stopwatch.ms() - 200) / 24), 0);

		// 会話ボックスを描画する
		rect.rounded(10).drawShadow(Vec2{ 1, 1 }, 8).draw().drawFrame(2, ColorF{ 0.4 });

		// 会話を描画する
		font(messages[messageIndex].substr(0, count)).draw(28, rect.stretched(-36, -20), ColorF{ 0.2 });

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

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

void Main()
{
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });
	const Font font{ FontMethod::MSDF, 48, Typeface::Medium };

	DialogBox dialogBox;
	dialogBox.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.",
	};
	dialogBox.stopwatch.start();

	while (System::Update())
	{
		dialogBox.update();
		dialogBox.draw(font);
	}
}

40.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;
			}
		}
	}
}

40.10 徐々に変化する数値

# include <Siv3D.hpp>

struct SmoothedInt
{
	double current = 0.0;

	int32 target = 0;

	double velocity = 0.0;

	void update()
	{
		current = Math::SmoothDamp(current, target, velocity, 0.3);
	}

	int32 rounded() const
	{
		return static_cast<int32>(Math::Round(current));
	}
};

void Main()
{
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });
	const Font font{ FontMethod::MSDF, 48, Typeface::Bold };

	SmoothedInt value;

	while (System::Update())
	{
		value.update();

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

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

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

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

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

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

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

40.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.1 });
	}
};

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

	int32 oldOrder = 0;

	Vec2 offset{ 0, 0 };
};

Optional<GrabbedItem> GrabItem(const Array<Item>& items, const Point& basePos)
{
	for (int32 order = 0; auto & item : items)
	{
		// アイテムの長方形
		const RectF rect = item.getRect(basePos, order);

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

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

			break;
		}

		++order;
	}

	return none;
}

void RearrangeItems(Array<Item>& items, int32 targetOrder, const GrabbedItem& grabbedItem)
{
	// 以前と異なるリスト内順序の場合
	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));
	}
}

void DrawItems(const Array<Item>& items, const Point& basePos, const Font& font,
	const Optional<int32>& targetOrder, const Optional<GrabbedItem>& grabbedItem)
{
	for (int32 order = 0; const auto & item : items)
	{
		// そこに挿入しようとしていたら、その位置をスキップする
		if (targetOrder == order)
		{
			++order;
		}

		// 現在掴んでいるアイテムは、元あった場所には描画しない
		if (grabbedItem && (grabbedItem->id == item.id))
		{
			continue;
		}

		item.draw(basePos, font, order);

		++order;
	}
}

void DrawGrabbedItem(const Array<Item>& items, const Point& basePos, const Font& font, const GrabbedItem& grabbedItem)
{
	// 掴んでいるアイテムだけを描画する
	for (const auto& item : items)
	{
		if (grabbedItem.id == item.id)
		{
			item.drawGrabbed(basePos, font, grabbedItem.offset, grabbedItem.oldOrder);

			return;
		}
	}
}

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)
		{
			grabbedItem = GrabItem(items, basePos);
		}

		// 掴んでいるアイテムの真下のリスト内順序。アイテムを掴んでいない場合は none
		Optional<int32> targetOrder;

		if (grabbedItem)
		{
			targetOrder = Clamp(((Cursor::Pos().y - basePos.y) / 80), 0, (static_cast<int32>(items.size()) - 1));

			// 掴んでいるアイテムを置く処理
			if (MouseL.up())
			{
				// アイテムの並び順を変更する
				RearrangeItems(items, *targetOrder, *grabbedItem);

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

			Cursor::RequestStyle(CursorStyle::Hand);
		}

		// リスト上のアイテムを描画する
		DrawItems(items, basePos, font, targetOrder, grabbedItem);

		// 掴んでいるアイテムを描画する
		if (grabbedItem)
		{
			DrawGrabbedItem(items, basePos, font, *grabbedItem);
		}
	}
}