Skip to content

アドオンのサンプル

ゲームやアプリにおいて特定のタイミングだけ有効にしたい機能は、アドオンとして実装すると、コード(Main() の中など)を汚しません。ここでは、アドオンで実装できるいくつかの機能のサンプルを紹介します。

1. ロード中の円の表示

コード
# include <Siv3D.hpp>

/// @brief ロード中の円を描画するアドオン
class LoadingCircleAddon : public IAddon
{
public:

	/// @brief ロード中の円の描画を開始します。
	/// @param circle 円
	/// @param thickness 軌跡の太さ 
	/// @param color 軌跡の色
	static void Begin(const Circle& circle, double thickness, const ColorF& color)
	{
		if (auto p = Addon::GetAddon<LoadingCircleAddon>(U"LoadingCircleAddon"))
		{
			p->begin(circle, thickness, color);
		}
	}

	/// @brief ロード中の円の描画を終了します。
	static void End()
	{
		if (auto p = Addon::GetAddon<LoadingCircleAddon>(U"LoadingCircleAddon"))
		{
			p->end();
		}
	}

	/// @brief ロード中の円の描画が有効かを返します。
	[[nodiscard]]
	static bool IsActive()
	{
		if (auto p = Addon::GetAddon<LoadingCircleAddon>(U"LoadingCircleAddon"))
		{
			return p->m_active;
		}
		else
		{
			return false;
		}
	}

private:

	bool init() override
	{
		m_trail = Trail{ LifeTime, [](double) { return 1.0; }, EaseOutExpo };

		return true;
	}

	bool update() override
	{
		if (not m_active)
		{
			return true;
		}

		m_accumulatedTime += Scene::DeltaTime();

		while (UpdateInterval <= m_accumulatedTime)
		{
			m_theta = Math::NormalizeAngle(m_theta + AngleStep);

			const Vec2 pos = OffsetCircular{ m_circle.center, m_circle.r, m_theta };

			m_trail.update(UpdateInterval);

			m_trail.add(pos, m_color, m_thickness);

			m_accumulatedTime -= UpdateInterval;
		}

		return true;
	}

	void draw() const override
	{
		if (not m_active)
		{
			return;
		}

		m_trail.draw();
	}

	static constexpr double LifeTime = 1.5;

	static constexpr double UpdateInterval = (1.0 / 120.0);

	static constexpr double AngleStep = 1.6_deg;

	Circle m_circle{ 0, 0, 0 };

	double m_thickness = 0.0;

	ColorF m_color = Palette::White;

	Trail m_trail;

	double m_accumulatedTime = 0.0;

	double m_theta = 180_deg;

	bool m_active = false;

	void begin(const Circle& circle, double thickness, const ColorF& color)
	{
		m_circle = circle;
		m_thickness = thickness;
		m_color = color;
		m_active = true;

		// 開始時点で十分な長さの軌跡を生成しておく
		prewarm();
	}

	void end()
	{
		m_active = false;
	}

	void prewarm()
	{
		// 前回の軌跡を消去する。v0.6.14 では m_trail.clear() が使える
		m_trail.update(LifeTime);

		m_accumulatedTime = LifeTime;

		m_theta = 180_deg;

		update();
	}
};

void Main()
{
	// アドオンを登録する
	Addon::Register<LoadingCircleAddon>(U"LoadingCircleAddon");

	while (System::Update())
	{
		if (const bool isActive = LoadingCircleAddon::IsActive();
			SimpleGUI::Button((isActive ? U"\U000F04DB" : U"\U000F040A"), Vec2{ 40, 40 }, 60))
		{
			if (not isActive)
			{
				// ロード中の円の描画を開始する
				LoadingCircleAddon::Begin(Circle{ 400, 300, 80 }, 10, ColorF{ 0.8, 0.9, 1.0 });
			}
			else
			{
				// ロード中の円の描画を終了する
				LoadingCircleAddon::End();
			}
		}
	}
}

2. メッセージの通知

コード
# include <Siv3D.hpp>

/// @brief 通知を管理するアドオン
class NotificationAddon : public IAddon
{
public:

	/// @brief 通知の種類
	enum class Type
	{
		/// @brief 通常
		Normal,

		/// @brief 情報
		Information,

		/// @brief 疑問
		Question,

		/// @brief 成功
		Success,

		/// @brief 警告
		Warning,

		/// @brief 失敗
		Failure,
	};

	/// @brief 通知のスタイル
	struct Style
	{
		/// @brief 通知の幅
		double width = 300.0;

		/// @brief 通知の背景色
		ColorF backgroundColor{ 0.0, 0.8 };

		/// @brief 通知の枠線色
		ColorF frameColor{ 0.75 };

		/// @brief 通知の文字色
		ColorF textColor{ 1.0 };

		/// @brief 情報アイコンの色
		ColorF informationColor{ 0.0, 0.72, 0.83 };

		/// @brief 疑問アイコンの色
		ColorF questionColor{ 0.39, 0.87, 0.09 };

		/// @brief 成功アイコンの色
		ColorF successColor{ 0.0, 0.78, 0.33 };

		/// @brief 警告アイコンの色
		ColorF warningColor{ 1.0, 0.57, 0.0 };

		/// @brief 失敗アイコンの色
		ColorF failureColor{ 1.00, 0.32, 0.32 };
	};

	/// @brief 通知を表示します。
	/// @param message メッセージ
	/// @param type 通知の種類
	static void Show(const StringView message, const Type type = NotificationAddon::Type::Normal)
	{
		if (auto p = Addon::GetAddon<NotificationAddon>(U"NotificationAddon"))
		{
			p->show(message, type);
		}
	}

	/// @brief 通知の表示時間を設定します。
	/// @param lifeTime 表示時間(秒)
	static void SetLifeTime(const double lifeTime)
	{
		if (auto p = Addon::GetAddon<NotificationAddon>(U"NotificationAddon"))
		{
			p->m_lifeTime = lifeTime;
		}
	}

	/// @brief 通知のスタイルを設定します。
	/// @param style スタイル
	static void SetStyle(const Style& style)
	{
		if (auto p = Addon::GetAddon<NotificationAddon>(U"NotificationAddon"))
		{
			p->m_style = style;
		}
	}

private:

	static constexpr StringView Icons = U" \U000F02FC\U000F02D7\U000F0E1E\U000F0029\U000F1398";

	struct Notification
	{
		String message;

		double time = 0.0;

		double currentIndex = 0.0;

		double velocity = 0.0;

		Type type = Type::Normal;
	};

	Style m_style;

	Array<Notification> m_notifications;

	double m_lifeTime = 10.0;

	bool update() override
	{
		const double deltaTime = Scene::DeltaTime();

		for (auto& notification : m_notifications)
		{
			notification.time += deltaTime;
		}

		m_notifications.remove_if([lifeTime = m_lifeTime](const Notification& notification) { return (lifeTime < notification.time); });

		for (size_t i = 0; i < m_notifications.size(); ++i)
		{
			auto& notification = m_notifications[i];
			notification.currentIndex = Math::SmoothDamp(notification.currentIndex,
				static_cast<double>(i), notification.velocity, 0.15, 9999.0, deltaTime);
		}

		return true;
	}

	void draw() const override
	{
		const Font& font = SimpleGUI::GetFont();

		for (const auto& notification : m_notifications)
		{
			double xScale = 1.0;
			double alpha = 1.0;

			if (notification.time < 0.2)
			{
				xScale = alpha = (notification.time / 0.2);
			}
			else if ((m_lifeTime - 0.4) < notification.time)
			{
				alpha = ((m_lifeTime - notification.time) / 0.4);
			}

			alpha = EaseOutExpo(alpha);
			xScale = EaseOutExpo(xScale);

			ColorF backgroundColor = m_style.backgroundColor;
			backgroundColor.a *= alpha;

			ColorF frameColor = m_style.frameColor;
			frameColor.a *= alpha;

			ColorF textColor = m_style.textColor;
			textColor.a *= alpha;

			const RectF rect{ 10, (10 + notification.currentIndex * 32), (m_style.width * xScale), 31 };
			rect.rounded(3).draw(backgroundColor).drawFrame(1, 0, frameColor);

			if (notification.type != Type::Normal)
			{
				ColorF color = notification.type == Type::Information ? m_style.informationColor
					: notification.type == Type::Question ? m_style.questionColor
					: notification.type == Type::Success ? m_style.successColor
					: notification.type == Type::Warning ? m_style.warningColor
					: m_style.failureColor;
				color.a *= alpha;

				font(Icons[FromEnum(notification.type)]).draw(18, Arg::leftCenter = rect.leftCenter().movedBy(8, -1), color);
			}

			font(notification.message).draw(18, Arg::leftCenter = rect.leftCenter().movedBy(32, -1), textColor);
		}
	}

	void show(const StringView message, const Type type)
	{
		const double currentIndex = (m_notifications.empty() ? 0.0 : m_notifications.back().currentIndex + 1.0);
		const double velocity = (m_notifications.empty() ? 0.0 : m_notifications.back().velocity);

		m_notifications << Notification{
			.message = String{ message },
			.time = 0.0,
			.currentIndex = currentIndex,
			.velocity = velocity,
			.type = type };
	}
};

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

	// アドオンを登録する
	Addon::Register<NotificationAddon>(U"NotificationAddon");

	while (System::Update())
	{
		if (SimpleGUI::Button(U"normal", Vec2{ 600, 40 }, 160))
		{
			NotificationAddon::Show(U"normal");
		}

		if (SimpleGUI::Button(U"information", Vec2{ 600, 80 }, 160))
		{
			NotificationAddon::Show(U"information", NotificationAddon::Type::Information);
		}

		if (SimpleGUI::Button(U"question", Vec2{ 600, 120 }, 160))
		{
			NotificationAddon::Show(U"question", NotificationAddon::Type::Question);
		}

		if (SimpleGUI::Button(U"success", Vec2{ 600, 160 }, 160))
		{
			NotificationAddon::Show(U"success", NotificationAddon::Type::Success);
		}

		if (SimpleGUI::Button(U"warning", Vec2{ 600, 200 }, 160))
		{
			NotificationAddon::Show(U"warning", NotificationAddon::Type::Warning);
		}

		if (SimpleGUI::Button(U"failure", Vec2{ 600, 240 }, 160))
		{
			NotificationAddon::Show(U"failure", NotificationAddon::Type::Failure);
		}
	}
} 

3. ホモグラフィ変換を適用した 2D 描画

コード
# include <Siv3D.hpp>

/// @brief ホモグラフィ変換を適用した 2D 描画を行うアドオン
class HomographyAddon : public IAddon
{
public:

	/// @brief ホモグラフィ変換を適用した 2D 描画を行います。
	/// @param quad 射影先の四角形
	/// @param texture 描画するテクスチャ
	static void Draw(const Quad& quad, const Texture& texture)
	{
		if (auto p = Addon::GetAddon<HomographyAddon>(U"HomographyAddon"))
		{
			p->setQuad(quad);
			p->draw(texture);
		}
	}

private:

	bool init() override
	{
		setQuad(Quad{ Scene::Rect() });
		return (m_vs && m_ps);
	}

	struct Homography
	{
		Float4 m1;
		Float4 m2;
		Float4 m3;
	};

	VertexShader m_vs = HLSL{ U"example/shader/hlsl/homography.hlsl", U"VS" }
		| GLSL{ U"example/shader/glsl/homography.vert", { { U"VSConstants2D", 0 }, { U"VSHomography", 1 } } };

	PixelShader m_ps = HLSL{ U"example/shader/hlsl/homography.hlsl", U"PS" }
		| GLSL{ U"example/shader/glsl/homography.frag", { { U"PSConstants2D", 0 }, { U"PSHomography", 1 } } };

	ConstantBuffer<Homography> m_vsConstant;

	ConstantBuffer<Homography> m_psConstant;

	Quad m_quad{};

	void setQuad(const Quad& quad)
	{
		if (m_quad == quad)
		{
			return;
		}

		m_quad = quad;
		const Mat3x3 mat = Mat3x3::Homography(quad);
		m_vsConstant = { Float4{ mat._11_12_13, 0 }, Float4{ mat._21_22_23, 0 }, Float4{ mat._31_32_33, 0 } };

		const Mat3x3 inv = mat.inverse();
		m_psConstant = { Float4{ inv._11_12_13, 0 }, Float4{ inv._21_22_23, 0 }, Float4{ inv._31_32_33, 0 } };
	}

	void draw(const Texture& texture) const
	{
		const ScopedCustomShader2D shader{ m_vs, m_ps };
		const ScopedRenderStates2D sampler{ SamplerState::ClampAniso };
		Graphics2D::SetVSConstantBuffer(1, m_vsConstant);
		Graphics2D::SetPSConstantBuffer(1, m_psConstant);
		Rect{ 1 }(texture).draw();
	}
};

void Main()
{
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });
	const MSRenderTexture renderTexture{ Size{ 800, 600 } };
	const Texture texture{ U"example/windmill.png", TextureDesc::Mipped };

	// アドオンを登録する
	Addon::Register<HomographyAddon>(U"HomographyAddon");

	const Quad q1{ Vec2{ 150, 300 }, Vec2{ 650, 300 }, Vec2{ 800, 600 }, Vec2{ 0, 600 } };
	const Quad q2{ Vec2{ 400, 50 }, Vec2{ 800, 0 }, Vec2{ 800, 300 }, Vec2{ 400, 250 } };

	// q1 から Scene::Rect() へのホモグラフィ変換行列
	const Mat3x3 mat = Mat3x3::Homography(q1, Rect{ 800, 600 }.asQuad());

	while (System::Update())
	{
		// q1 上の座標を Scene::Rect() 上の座標に変換してセルのインデックスを計算する
		const Point index = (mat.transformPoint(Cursor::Pos()).asPoint() / 40);
		{
			const ScopedRenderTarget2D target{ renderTexture.clear(ColorF{ 1.0 }) };

			for (int32 y = 0; y < 15; ++y)
			{
				for (int32 x = 0; x < 20; ++x)
				{
					if (Point{ x, y } == index)
					{
						Rect{ (x * 40), (y * 40), 40 }.draw(ColorF{ 0.25 });
					}
					else if (IsEven(y + x))
					{
						Rect{ (x * 40), (y * 40), 40 }.draw(ColorF{ 0.75 });
					}
				}
			}
		}

		// MSRenderTexture の内容確定と resolve
		{
			Graphics2D::Flush();
			renderTexture.resolve();
		}

		HomographyAddon::Draw(q1, renderTexture);
		HomographyAddon::Draw(q2, texture);
	}
}