OpenSiv3D v0.6.4 新規追加・更新サンプル
3D 描画時の UVTransform
Mesh
, Box
, OrientedBox
, Plane
の描画関数に TextureRegion
を渡せるようになりました。
この機能の追加に伴い、標準頂点シェーダに新しい標準定数バッファが追加されたため、カスタム頂点テクスチャを使用している場合は注意が必要です。
# 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);
}
}
}
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"));
}
}
Polygon::addHole() の改良
Polygon::addHole()
に RectF
や Circle
, 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 });
}
}
NavMesh のコンストラクタでマップを構築可能に
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);
}
}
-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 });
}
}
MeshData::RoundedBox()
角が丸い直方体メッシュデータを作れる MeshData::RoundedBox()
を追加しました。
# 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);
}
}
}
Shader::LinearToScreen() でのテクスチャフィルタ指定を可能に
Shader::LinearToScreen()
で 3D 描画結果を 2D シーンにコピーする際に、最近傍補間を使えるようになりました。低解像度風の 3D 映像のレンダリングに役立ちます。
# 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);
}
}
}
DisjointSet (Union Find)
競技プログラミング等で用いられるデータ構造・アルゴリズムの Disjoint Set (Union-Find) を実現するクラス DisjointSet
を追加しました。
# 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);
}
}
}
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);
}
}
}
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())
{
}
}
Photon SDK と連係したマルチプレイ通信機能
チュートリアル
チャット
# 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;
}
}
}
}
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();
}
}
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();
}
}