Open13

OpenSiv3D v0.6.4 新規追加・更新サンプル

Ryo SuzukiRyo Suzuki

3D 描画時の UVTransform

Mesh, Box, OrientedBox, Plane の描画関数に TextureRegion を渡せるようになりました。
この機能の追加に伴い、標準頂点シェーダに新しい標準定数バッファが追加されたため、カスタム頂点テクスチャを使用している場合は注意が必要です。

https://twitter.com/Reputeless/status/1514597478397804545

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

void Main()
{
	Window::Resize(1280, 720);

	const ColorF backgroundColor = ColorF{ 0.4, 0.6, 0.8 }.removeSRGBCurve();
	const Texture uvChecker{ U"example/texture/uv.png", TextureDesc::MippedSRGB };
	const Texture earthTexture{ U"example/texture/earth.jpg", TextureDesc::MippedSRGB };
	const MSRenderTexture renderTexture{ Scene::Size(), TextureFormat::R8G8B8A8_Unorm_SRGB, HasDepth::Yes };
	DebugCamera3D camera{ renderTexture.size(), 30_deg, Vec3{ 10, 16, -32 } };

	while (System::Update())
	{
		camera.update(2.0);
		Graphics3D::SetCameraTransform(camera);

		// 3D 描画
		{
			const ScopedRenderTarget3D target{ renderTexture.clear(backgroundColor) };

			Plane{ 64 }.draw(ColorF{ 0.7, 0.9, 0.8 }.removeSRGBCurve());

			const double t = Fraction(Scene::Time() * 0.125);

			const double tx = Floor(t * 8) / 8.0;

			Plane{ Vec3{0, 0.05, 0}, 4 }.draw(uvChecker.uv(0.0, t, 0.125, 0.125));

			Box{ 6, 2, 0, 4 }.draw(uvChecker.uv(t, 0.0, 0.25, 0.25));

			Box{ 1, 5, 3, 2 }.oriented(Quaternion::RollPitchYaw(Scene::Time() * 10_deg, Scene::Time() * 70_deg, 0.0f)).draw(uvChecker.uv(tx, 0.0, 0.125, 0.125));
		}

		// 3D シーンを 2D シーンに描画
		{
			Graphics3D::Flush();
			renderTexture.resolve();
			Shader::LinearToScreen(renderTexture);
		}
	}
}
Ryo SuzukiRyo Suzuki

Font にリガチャ(合字)を回避するオプションを追加

文字列のグリフ配列を取得する Font のメンバ関数において、リガチャ(合字)の有無を設定できるようにし、Font::getGlyphs() についてはデフォルトで合字を無効化するようにしました。

リガチャに関する参考記事

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

void Draw(const Vec2& basePos, const Array<Glyph>& glyphs)
{
	Vec2 penPos{ basePos };

	for (const auto& glyph : glyphs)
	{
		glyph.texture.draw(Math::Round(penPos + glyph.getOffset()));

		penPos.x += (glyph.xAdvance + 30);
	}
}

void Main()
{
	const Font font{ 40 };

	while (System::Update())
	{
		// v0.6.3 まで(リガチャが有効)
		Draw(Vec2{ 40, 40 }, font.getGlyphs(U"Efficient", Ligature::Yes));

		// v0.6.4(リガチャがデフォルトで無効)
		Draw(Vec2{ 40, 140 }, font.getGlyphs(U"Efficient"));
	}
}
Ryo SuzukiRyo Suzuki

Polygon::addHole() の改良

Polygon::addHole()RectFCircle, Quad などの図形を渡せるオーバーロードを追加しました。また、穴の追加に失敗した場合、自身は変更せずに bool 型の戻り値で false を返すようになりました。

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

void Main()
{
	Polygon polygon = Scene::Rect().stretched(-40).asPolygon();

	while (System::Update())
	{
		const Rect rect{ Arg::center = Cursor::Pos(), 60, 40 };

		if (MouseL.down())
		{
			if (not polygon.addHole(rect))
			{
				Print << U"Failed";
			}
		}

		polygon.draw(ColorF{ 0.8, 0.9, 1.0 });

		rect.draw(ColorF{ 1.0, 0.5 });
	}
}
Ryo SuzukiRyo Suzuki

NavMesh のコンストラクタに地形データを渡し、コンストラクタ内でビルドできるようになりました。ナビメッシュを使うコードを少し短くできます。

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

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

	Polygon polygon = Scene::Rect().stretched(-40).asPolygon();
	polygon.addHole(Rect{ 100, 100, 200, 200 });
	polygon.addHole(Rect{ 200, 350, 150, 100 });
	polygon.addHole(Rect{ 500, 200, 200, 200 });

	const Vec2 start{ 70, 70 };
	NavMesh navMesh{ polygon, NavMeshConfig{ .agentRadius = 10 } };

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

		const Vec2 goal = Cursor::Pos();
		const LineString lines(navMesh.query(start, goal));

		lines.draw(5, Palette::Red);
		start.asCircle(8).draw(Palette::Red);
		goal.asCircle(8).draw(Palette::Red);
	}
}
Ryo SuzukiRyo Suzuki

-1.0 ~ 1.0 の範囲を返す Periodic 関数

従来の Periodic:: 系の関数は 0.0~1.0 を返しましたが、-1.0~1.0 を返す関数が追加されました。

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

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

	while (System::Update())
	{
		const double p0 = Periodic::Square1_1(2s);
		const double p1 = Periodic::Triangle1_1(2s);
		const double p2 = Periodic::Sine1_1(2s);
		const double p3 = Periodic::Sawtooth1_1(2s);
		const double p4 = Periodic::Jump1_1(2s);

		Line{ 100, 0, 100, 600 }.draw(2, ColorF{ 0.8 });
		Line{ 700, 0, 700, 600 }.draw(2, ColorF{ 0.8 });

		Circle{ (400 + p0 * 300), 100, 20 }.draw(ColorF{ 0.25 });
		Circle{ (400 + p1 * 300), 200, 20 }.draw(ColorF{ 0.25 });
		Circle{ (400 + p2 * 300), 300, 20 }.draw(ColorF{ 0.25 });
		Circle{ (400 + p3 * 300), 400, 20 }.draw(ColorF{ 0.25 });
		Circle{ (400 + p4 * 300), 500, 20 }.draw(ColorF{ 0.25 });
	}
}
Ryo SuzukiRyo Suzuki

MeshData::RoundedBox()

角が丸い直方体メッシュデータを作れる MeshData::RoundedBox() を追加しました。

https://twitter.com/Reputeless/status/1514225459143507973

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

void Main()
{
	Window::Resize(1280, 720);

	const PixelShader ps = HLSL{ U"example/shader/hlsl/forward_triplanar.hlsl", U"PS" }
		| GLSL{ U"example/shader/glsl/forward_triplanar.frag", {{ U"PSPerFrame", 0 }, { U"PSPerView", 1 }, { U"PSPerMaterial", 3 }} };

	if (not ps)
	{
		return;
	}

	const ColorF backgroundColor = ColorF{ 0.4, 0.6, 0.8 }.removeSRGBCurve();
	const Texture uvChecker{ U"example/texture/uv.png", TextureDesc::MippedSRGB };
	const Texture woodTexture{ U"example/texture/wood.jpg", TextureDesc::MippedSRGB };
	const MSRenderTexture renderTexture{ Scene::Size(), TextureFormat::R8G8B8A8_Unorm_SRGB, HasDepth::Yes };
	DebugCamera3D camera{ renderTexture.size(), 30_deg, Vec3{ 10, 16, -32 } };

	const Mesh mesh0{ MeshData::RoundedBox(1, Vec3{ 4, 4, 4 }, 1) };
	const Mesh mesh1{ MeshData::RoundedBox(1, Vec3{ 4, 4, 4 }, 2) };
	const Mesh mesh2{ MeshData::RoundedBox(1, Vec3{ 4, 4, 4 }, 3) };
	const Mesh mesh3{ MeshData::RoundedBox(1, Vec3{ 4, 4, 4 }, 4) };

	const Mesh meshA1{ MeshData::RoundedBox(0.5, Vec3{ 4, 4, 1 }, 4) };
	const Mesh meshA2{ MeshData::RoundedBox(1.7, Vec3{ 4, 4, 4 }, 4) };
	const Mesh meshA3{ MeshData::RoundedBox(2.0, Vec3{ 4, 6, 4 }, 4) };

	while (System::Update())
	{
		camera.update(2.0);

		// 3D
		{
			Graphics3D::SetCameraTransform(camera);

			const ScopedRenderTarget3D target{ renderTexture.clear(backgroundColor) };

			Plane{ 64 }.draw(uvChecker);

			{
				const ScopedRenderStates3D wireframe{ RasterizerState::WireframeCullNone };
				const ColorF color{ 0.0 };
				mesh0.draw(Vec3{ -12, 2, 6 }, color);
				mesh1.draw(Vec3{ -6, 2, 6 }, color);
				mesh2.draw(Vec3{ 0, 2, 6 }, color);
				mesh3.draw(Vec3{ 6, 2, 6 }, color);
			}

			mesh0.draw(Vec3{ -12, 2, 0 });
			mesh1.draw(Vec3{ -6, 2, 0 });
			mesh2.draw(Vec3{ 0, 2, 0 });
			mesh3.draw(Vec3{ 6, 2, 0 });

			{
				const ScopedCustomShader3D shader{ ps };
				mesh0.draw({ -12, 2, -6 }, woodTexture);
				mesh1.draw({ -6, 2, -6 }, woodTexture);
				mesh2.draw({ 0, 2, -6 }, woodTexture);
				mesh3.draw({ 6, 2, -6 }, woodTexture);

				meshA1.draw({ 12, 2, -12 }, woodTexture);
				meshA2.draw({ 18, 2, -12 }, woodTexture);
				meshA3.draw({ 24, 3, -12 }, woodTexture);
			}
		}

		// RenderTexture を 2D シーンに描画
		{
			Graphics3D::Flush();
			renderTexture.resolve();
			Shader::LinearToScreen(renderTexture);
		}
	}
}
Ryo SuzukiRyo Suzuki

Shader::LinearToScreen() でのテクスチャフィルタ指定を可能に

Shader::LinearToScreen() で 3D 描画結果を 2D シーンにコピーする際に、最近傍補間を使えるようになりました。低解像度風の 3D 映像のレンダリングに役立ちます。

https://twitter.com/Reputeless/status/1512688366663647232

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

void Main()
{
	constexpr Size SceneSize{ 256, 192 };
	const ColorF backgroundColor = ColorF{ 0.4, 0.6, 0.8 }.removeSRGBCurve();

	const Texture wiindmill{ Image{ U"example/windmill.png" }.clipped(200, 230, 64, 64), TextureDesc::UnmippedSRGB };
	const Texture siv3dKun{ Image{ U"example/spritesheet/siv3d-kun-16.png" }.clipped(0, 0, 20, 32), TextureDesc::UnmippedSRGB };

	const Mesh spriteMesh{ MeshData::TwoSidedPlane(SizeF{ 2.0, 3.2 }).rotate(Quaternion::RotateX(-90_deg)) };
	const RenderTexture renderTexture{ SceneSize, TextureFormat::R8G8B8A8_Unorm_SRGB, HasDepth::Yes };

	DebugCamera3D camera{ renderTexture.size(), 30_deg, Vec3{ 10, 2, -32 } };

	while (System::Update())
	{
		camera.update(2.0);
		Graphics3D::SetCameraTransform(camera);

		// [3D rendering]
		{
			const ScopedRenderTarget3D target{ renderTexture.clear(backgroundColor) };

			Plane{ 64 }.draw(ColorF{ 0.7 }.removeSRGBCurve());
			Box::FromPoints(Vec3{ -4, 0, -4 }, Vec3{ -2, 4, 4 }).draw(ColorF{ 0.8, 0.6, 0.4 }.removeSRGBCurve());
			Plane{ Vec3{0, 4, 0 }, 64 }.draw(ColorF{ 0.5 }.removeSRGBCurve());

			{
				const ScopedRenderStates3D sampler{ SamplerState::ClampNearest };
				Box{ 4, 2, 0, 4 }.draw(wiindmill);
			}

			{
				const ScopedRenderStates3D sampler{ SamplerState::ClampNearest, BlendState::Default2D };
				spriteMesh.draw(Vec3{ 0, 1.6, -4 }, siv3dKun);
			}
		}

		// [2D rendering]
		{
			Graphics3D::Flush();

			// TextureFilter::Nearest
			Shader::LinearToScreen(renderTexture, TextureFilter::Nearest);
		}
	}
}
Ryo SuzukiRyo Suzuki

DisjointSet (Union Find)

競技プログラミング等で用いられるデータ構造・アルゴリズムの Disjoint Set (Union-Find) を実現するクラス DisjointSet を追加しました。

https://twitter.com/Reputeless/status/1497494547546324999

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

void Main()
{
	Window::Resize(1280, 720);
	Scene::SetBackground(ColorF{ 0.8, 0.9, 1.0 });

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

	// セルの大きさ
	constexpr int32 CellSize = 16;

	// マス目の数
	constexpr Size GridSize{ 1280 / CellSize, 720 / CellSize };

	// 塗りつぶし (白: true, 黒: false)
	Grid<bool> grid(GridSize, true);

	// Disjoint Set (Union-Find)
	DisjointSet<int32> ds{ GridSize.x * GridSize.y };

	// 現在存在する領域の root と, 領域の座標の合計値 (中心計算用)
	HashTable<int32, Vec2> currentRoots;

	// root の番号と色 (hue) の対応表
	HashTable<int32, int32> globalColorTable;
	int32 colorIndex = 0;

	// UnionFind を更新する必要があるか
	bool isDirty = true;

	while (System::Update())
	{
		if (isDirty)
		{
			// Disjoint Set を更新
			{
				ds.reset();

				for (int32 y = 0; y < GridSize.y; ++y)
				{
					for (int32 x = 0; x < GridSize.x; ++x)
					{
						if (grid[y][x])
						{
							const int32 index = (y * GridSize.x + x);

							if (int32 nx = (x + 1); nx < GridSize.x)
							{
								if (grid[y][nx])
								{
									ds.merge(index, index + 1);
								}
							}

							if (int32 ny = (y + 1); ny < GridSize.y)
							{
								if (grid[ny][x])
								{
									ds.merge(index, (index + GridSize.x));
								}
							}
						}
					}
				}
			}

			// 存在する root 一覧を作成
			{
				currentRoots.clear();

				for (int32 y = 0; y < GridSize.y; ++y)
				{
					for (int32 x = 0; x < GridSize.x; ++x)
					{
						if (grid[y][x])
						{
							const int32 index = (y * GridSize.x + x);
							const int32 root = ds.find(index);
							const Vec2 pos{ x, y };

							if (auto it = currentRoots.find(root); it == currentRoots.end())
							{
								currentRoots.emplace(root, pos);
							}
							else
							{
								it->second += pos;
							}
						}
					}
				}
			}

			// root と色の対応表を更新
			{
				for (auto& currentRoot : currentRoots)
				{
					if (not globalColorTable.contains(currentRoot.first))
					{
						globalColorTable.emplace(currentRoot.first, (colorIndex++ * 55));
					}
				}

				EraseNodes_if(globalColorTable, [&](const auto& p) { return (not currentRoots.contains(p.first)); });
			}

			isDirty = false;
		}

		// すべてのマスを描画
		for (auto p : step(GridSize))
		{
			const Rect rect = Rect{ (p * CellSize), CellSize }.stretched(-1);

			if (grid[p])
			{
				const int32 index = (p.y * GridSize.x + p.x);
				const int32 root = ds.find(index);
				rect.draw(HSV{ globalColorTable[root], 0.25, 1.0 });
			}
			else
			{
				rect.draw(ColorF{ 0.4 });
			}
		}

		// クリックされたらマスの状態を更新
		if ((MouseL | MouseR).pressed())
		{
			const Point pos = (Cursor::Pos() / CellSize);

			if (InRange(pos.x, 0, (GridSize.x - 1))
				&& InRange(pos.y, 0, (GridSize.y - 1)))
			{
				const bool old = grid[pos];
				grid[pos] = MouseL.pressed() ? false : true;
				isDirty = (old != grid[pos]);
			}
		}

		// 領域の情報を表示
		for (const auto& currentRoot : currentRoots)
		{
			const int32 root = currentRoot.first;
			const int32 size = static_cast<int32>(ds.size(root));
			const Vec2 center = currentRoot.second / size;
			const HSV textColor = HSV{ globalColorTable[root], 0.55, 0.9 };
			const Vec2 pos = (center * CellSize) + (Vec2::All(CellSize) * 0.5);
			const double fontSize = (20 + 2 * Sqrt(size));
			const double w = font(size).region(fontSize).w;

			Circle{ pos, (w / 1.66 + 10) }.draw(ColorF{ 1.0, 0.88 }).drawFrame(3, textColor);
			font(size).drawAt(fontSize, pos, textColor);
		}
	}
}
Ryo SuzukiRyo Suzuki

Platform::Windows::ToastNotification::Show における通知音の無効化オプション

ToastNotificationItem に、通知音の有無を指定する bool audio プロパティを追加しました。デフォルトでは true です。

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

void Main()
{
	while (System::Update())
	{
		if (SimpleGUI::Button(U"Notify", Vec2{ 20,20 }))
		{
			Platform::Windows::ToastNotification::Clear();

			const ToastNotificationItem item
			{
				.title = U"Test 1",
				.message = U"Message 1",
			};

			Platform::Windows::ToastNotification::Show(item);
		}

		if (SimpleGUI::Button(U"Notify (with no sound)", Vec2{ 20,60 }))
		{
			Platform::Windows::ToastNotification::Clear();

			const ToastNotificationItem item
			{
				.title = U"Test 2",
				.message = U"Message 2",
				.audio = false
			};

			Platform::Windows::ToastNotification::Show(item);
		}
	}
}
Ryo SuzukiRyo Suzuki

AudioStream (DynamicAudio)

正弦波

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

class MyAudioStream : public IAudioStream
{
public:

	void setFrequency(int32 frequency)
	{
		m_oldFrequency = m_frequency.load();

		m_frequency = frequency;
	}

private:

	size_t m_pos = 0;

	std::atomic<int32> m_oldFrequency = 440;

	std::atomic<int32> m_frequency = 440;

	void getAudio(float* left, float* right, const size_t samplesToWrite) override
	{
		const int32 oldFrequency = m_oldFrequency;
		const int32 frequency = m_frequency;
		const float blend = (1.0f / samplesToWrite);

		for (size_t i = 0; i < samplesToWrite; ++i)
		{
			const float t0 = (2_piF * oldFrequency * (static_cast<float>(m_pos) / Wave::DefaultSampleRate));
			const float t1 = (2_piF * frequency * (static_cast<float>(m_pos) / Wave::DefaultSampleRate));
			const float a = (Math::Lerp(std::sin(t0), std::sin(t1), (blend * i))) * 0.5f;

			*left++ = *right++ = a;
			++m_pos;
		}

		m_oldFrequency = frequency;

		m_pos %= Math::LCM(frequency, Wave::DefaultSampleRate);
	}

	bool hasEnded() override
	{
		return false;
	}

	void rewind() override
	{
		m_pos = 0;
	}
};

void Main()
{
	std::shared_ptr<MyAudioStream> audioStream = std::make_shared<MyAudioStream>();

	Audio audio{ audioStream };

	audio.play();

	double frequency = 440.0;

	while (System::Update())
	{
		if (SimpleGUI::Slider(U"{}Hz"_fmt(static_cast<int32>(frequency)), frequency, 220.0, 880.0, Vec2{ 40, 40 }, 120, 200))
		{
			audioStream->setFrequency(static_cast<int32>(frequency));
		}
	}
}

マイク入力のリアルタイム書き込み

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

class MyAudioStream : public IAudioStream
{
public:

	explicit MyAudioStream(Microphone&& microphone)
		: m_microphone{ std::move(microphone) }
		, m_sampleRate{ m_microphone.getSampleRate() }
		, m_delaySamples{ (m_sampleRate * 2) } // 2 秒遅れで録音波形をコピー(もっと短くしても良い)
		, m_bufferLength{ m_microphone.getBufferLength() } {}

private:

	Microphone m_microphone;

	uint32 m_sampleRate = Wave::DefaultSampleRate;

	uint32 m_delaySamples = 0;

	size_t m_bufferLength = 0;

	size_t m_readPos = 0;

	bool m_initialized = false;

	void getAudio(float* left, float* right, const size_t samplesToWrite) override
	{
		if (not m_initialized)
		{
			// 録音が始まっていない場合は無視
			if (m_microphone.posSample() == 0)
			{
				return;
			}

			// 現在の録音サンプル位置から m_delaySamples サンプルだけ引いた位置を読み取り開始位置に
			m_readPos = (m_microphone.posSample() + (m_bufferLength - m_delaySamples)) % m_bufferLength;

			m_initialized = true;
		}

		const size_t tailLength = Min((m_bufferLength - m_readPos), samplesToWrite);
		const size_t headLength = (samplesToWrite - tailLength);
		const Wave& wave = m_microphone.getBuffer();

		for (size_t i = 0; i < tailLength; ++i)
		{
			const auto& sample = wave[m_readPos + i];
			*left++ = sample.left;
			*right++ = sample.right;
		}

		for (size_t i = 0; i < headLength; ++i)
		{
			const auto& sample = wave[i];
			*left++ = sample.left;
			*right++ = sample.right;
		}

		m_readPos = ((m_readPos + samplesToWrite) % m_bufferLength);
	}

	bool hasEnded() override
	{
		return false;
	}

	void rewind() override {}
};


void Main()
{
	if (System::EnumerateMicrophones().isEmpty())
	{
		throw Error{ U"No microphone is connected" };
	}

	std::shared_ptr<MyAudioStream> audioStream;
	uint32 sampleRate = Wave::DefaultSampleRate;
	{
		Microphone mic{ 5s, Loop::Yes, StartImmediately::Yes};

		if (not mic.isRecording())
		{
			throw Error{ U"Failed to start recording" };
		}

		sampleRate = mic.getSampleRate();
		audioStream = std::make_shared<MyAudioStream>(std::move(mic));
	}

	Audio audio{ audioStream, Arg::sampleRate = sampleRate };
	audio.play();

	while (System::Update())
	{

	}
}
Ryo SuzukiRyo Suzuki

Photon SDK と連係したマルチプレイ通信機能

チュートリアル

https://zenn.dev/reputeless/scraps/03d951dddb53ab

チャット

# include <Siv3D.hpp> // OpenSiv3D v0.6.4
# include "Multiplayer_Photon.hpp"
# include "PHOTON_APP_ID.SECRET"

class Chat : public Multiplayer_Photon
{
public:

	static constexpr int32 MaxPlayers = 8;
	static constexpr size_t MaxHistory = 100;
	using Multiplayer_Photon::Multiplayer_Photon;

	void addMessage(const String& message)
	{
		if (MaxHistory <= m_messages.size())
		{
			m_messages.pop_front();
		}

		m_messages << message;
	}

	const Array<String>& getMessages() const
	{
		return m_messages;
	}

private:

	Array<String> m_messages;

	void connectReturn(const int32 errorCode, const String&, const String&, const String&) override
	{
		if (errorCode)
		{
			return;
		}

		joinRandomRoom(MaxPlayers);
	}

	void joinRandomRoomReturn(const LocalPlayerID, const int32 errorCode, const String&) override
	{
		if (errorCode == NoRandomMatchFound)
		{
			const RoomName roomName = (getUserName() + U"'s room-" + ToHex(RandomUint32()));
			return createRoom(roomName, MaxPlayers);
		}
	}

	void customEventAction(const LocalPlayerID, const uint8, const String& data) override
	{
		addMessage(data);
	}
};

void Main()
{
	Window::Resize(1280, 720);
	Scene::SetBackground(ColorF{ 0.8, 0.9, 1.0 });
	const std::string secretAppID{ SIV3D_OBFUSCATE(PHOTON_APP_ID) };
	Chat network{ secretAppID, U"1.0", Verbose::No };

	TextEditState userName;
	ListBoxState chat;
	TextEditState te;

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

		if (not network.isActive())
		{
			SimpleGUI::Headline(U"Enter your name:", Vec2{ 40, 40 }, 240);
			SimpleGUI::TextBox(userName, Vec2{ 40, 100 }, 140, 8);
			if (SimpleGUI::Button(U"OK", Vec2{ 190, 100 }, 90, (0 < userName.text.size())))
			{
				network.connect(userName.text);
				userName.clear();
			}
		}
		else if (not network.isInRoom())
		{
			SimpleGUI::Headline(U"Connecting...", Vec2{ 40, 40 }, 240);
		}
		else
		{
			chat.items = network.getMessages();
			SimpleGUI::ListBox(chat, Vec2{ 40, 40 }, 1200, 560);

			const bool previous = te.active;
			bool sendByEnter = false;
			SimpleGUI::TextBox(te, Vec2{ 40, 620 }, 1000, 50);
			if (previous && (te.active == false))
			{
				sendByEnter = (te.text && TextInput::GetRawInput().includes(U'\r'));
			}

			if (SimpleGUI::Button(U"Send", Vec2{ 1060, 620 }) || sendByEnter)
			{
				const String message = (U"[" + network.getUserName() + U"]: " + te.text + DateTime::Now().format(U" (MM-dd HH:mm:ss)"));
				network.sendEvent(0, message);
				network.addMessage(message);
				te.clear();
				te.active = sendByEnter;
			}
		}
	}
}
Ryo SuzukiRyo Suzuki

Script 内で include したファイルを取得する

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

void Main()
{
	const ManagedScript script{ U"example/script/hello.as" };

	Print << script.getIncludedFiles();

	while (System::Update())
	{
		script.run();
	}
}
Ryo SuzukiRyo Suzuki

ManagedScript に、リロードを発生させるカスタムトリガーを設定する

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

void Main()
{
	ManagedScript script{ U"example/script/hello.as" };

	script.setTriggerToReload([](){ return (KeyControl + KeyR).down(); });

	while (System::Update())
	{
		script.run();
	}
}