Open8
「Siv3D ゲームジャム 2025」の参加人数と同じ数だけ Siv3D 記事を書く
ピン留めされたアイテム
- Siv3D ゲームジャム 2025: https://bandainamcostudios.connpass.com/event/364446/
- 記事は、書く速度優先で Zenn にまとめる。後日整理して公式サイトに移植する
Microsoft/Proxy ライブラリを用いて、Siv3D の図形型とユーザ定義の図形型を統一的に表現できる型を実現する
Microsoft/Proxy について
- 継承を使わず、あらかじめ定義した「ファサード(インターフェイス)」を満たす任意の型を共通のプロキシで扱える型消去ライブラリ(構造的ポリモーフィズム)
- GitHub: https://github.com/microsoft/proxy/
- ヘッダオンリーライブラリ。インクルードするだけで使える
継承と比べた利点
- Rect/Circle/RoundRect などに基底クラスが無くても、所定のメンバ関数名とシグネチャを満たせばそのまま扱える
- 既存型に継承や virtual を追加する必要がない非侵入性
std::variant と比べた利点
-
std::variant
は型の列挙を事前に固定する必要があり、追加時に定義とvisitor
を改修する必要がある。proxy
は「要件を満たす型なら何でも可」で、既存コードに手を入れず拡張できる -
std::visit
のボイラープレート不要。m_shape->draw()
のように直接呼べる
Siv3D での活用サンプル
-
Button
クラスはButtonShape
のファサード要件(draw()
,mouseOver()
メンバ関数)を満たす任意の型を所有できる - Siv3D の図形クラスはそのまま利用可能。ユーザ定義型も要件を満たすだけで利用可能
コード
# include <Siv3D.hpp>
// https://github.com/microsoft/proxy
// 型消去により、所定の「ファサード(インターフェイス)」を満たす任意型を共通のプロキシ(型消去ポインタ)で扱える
# include <proxy/proxy.h>
// メンバ関数呼び出し用の「ディスパッチタグ」を定義。
// 第 2 引数には実装側のメンバ関数名を渡す。具体的な関数シグネチャは後段の add_convention で指定する。
PRO_DEF_MEM_DISPATCH(MemDraw, draw);
PRO_DEF_MEM_DISPATCH(MemMouseOver, mouseOver);
// ButtonShape が要求する操作群(インターフェイス)を「ファサード」として宣言。
// add_convention<タグ, シグネチャ> を列挙し、build で確定する。
// これにより、異なる型でも draw(ColorF) と mouseOver() を持つものは同一の「ButtonShape」として扱える。
struct ButtonShape
: pro::facade_builder
::add_convention<MemDraw, void(const ColorF&) const>
::add_convention<MemMouseOver, bool() const>
::support_copy<pro::constraint_level::nontrivial>
::build {
};
// 与えられた型のポインタが ButtonShape のファサード要件を満たして proxy 化できるかを、コンパイル時にチェックするコンセプト
template <class Type>
concept ButtonShapeProxiable = pro::proxiable<std::add_pointer_t<std::remove_cvref_t<Type>>, ButtonShape>;
class Button
{
public:
[[nodiscard]]
bool mouseOver() const
{
// プロキシ経由で、実体のメンバ関数 mouseOver() を呼び出す
return m_shape->mouseOver();
}
[[nodiscard]]
bool down() const
{
return (mouseOver() && MouseL.down());
}
void draw() const
{
const bool mouseOver = this->mouseOver();
const bool pressed = (mouseOver && MouseL.pressed());
const ColorF color = (pressed ? PressedColor : (mouseOver ? HoverColor : DefaultColor));
// ここでもプロキシ経由で draw() を呼び出す。実体の型(Rect, Circle, 自作型など)に依存しない
m_shape->draw(color);
}
[[nodiscard]]
static Button Make(const ButtonShapeProxiable auto& shape)
{
Button button;
button.m_shape = pro::make_proxy<ButtonShape>(shape);
return button;
}
private:
static constexpr ColorF DefaultColor{ 0.9 };
static constexpr ColorF HoverColor{ 0.8, 0.9, 1.0 };
static constexpr ColorF PressedColor{ 0.7 };
// 異なるクラス(Rect, Circle, RoundRect, ユーザー定義型など)を
// 「ButtonShape」という共通インターフェイスで型消去して扱うためのプロキシ
pro::proxy<ButtonShape> m_shape;
};
class MyShape
{
public:
MyShape(const Circle& circle1, const Circle& circle2)
: m_circle1{ circle1 }
, m_circle2{ circle2 } {
}
// ButtonShape で要求されるシグネチャ(void(const ColorF&) const)に一致
void draw(const ColorF& color) const
{
m_circle1.draw(color);
m_circle2.draw(color);
}
// ButtonShape で要求されるシグネチャ(bool() const)に一致
bool mouseOver() const
{
return (m_circle1.mouseOver() || m_circle2.mouseOver());
}
private:
Circle m_circle1;
Circle m_circle2;
};
void Main()
{
Array<Button> buttons;
// Rect / Circle / RoundRect は Siv3D 側で draw(ColorF) const と mouseOver() const を提供しているため、
// ButtonShape ファサードに適合し、プロキシとして格納される
buttons << Button::Make(Rect{ 100, 100, 200, 100 });
buttons << Button::Make(Circle{ 200, 300, 50 });
buttons << Button::Make(RoundRect{ 100, 400, 200, 100, 20 });
// ユーザー定義型 MyShape も同じシグネチャのメンバ関数を実装しているので適合する
buttons << Button::Make(MyShape{ Circle{ 500, 200, 50 }, Circle{ 600, 200, 50 } });
while (System::Update())
{
for (const auto& button : buttons)
{
button.draw();
}
for (auto&& [index, button] : Indexed(buttons))
{
if (button.down())
{
Print << U"Button {} clicked"_fmt(index);
}
}
}
}
toml11 ライブラリを用いて、TOML v1.0.0 に対応する TOML パーサを作る
概要
- Siv3D v0.6 世代の
s3d::TOMLReader
は、使用している内部ライブラリ(tomlcpp)の制約で TOML v0.5.0 までのサポート - 本記事では toml11 を利用して、TOML v1.0.0 に対応した、Siv3D から使いやすい新しいパーサクラス
s3d::ext::TOML
を作成した
s3d::TOMLReader
からの変更点
既存の - TOML v1.0.0 をサポートした
- 文字列からのパースに対応した
-
JSON
と同様にTOML::Load(path)
またはTOML::Parse(content)
で作成するように
-
- LocalTime, LocalDate, LocalDateTime, OffsetDateTime を区別するようにした
-
.tableArrayView()
を.arrayView()
に統合するなどメンバ関数を整理した - 要素の変更や書き出し機能は引き続き未サポート(Siv3D v0.8 での実装待ち)
-
s3d::TOMLReader
でサポートされている、[U"Window.title"]
を[U"Window"][U"title"]
にする分割機能は、TOML の quoted key と相性が悪いため廃止した。これは Siv3D v0.8 の新クラスでも正式に廃止予定
ライブラリコード
サンプル 1
- v1.0.0 で許可される、異なる型が混在する配列に対応
test.toml
items = [10, "こんにちは", true]
コード
Main.cpp
# include <Siv3D.hpp>
# include "extTOML.hpp"
void Main()
{
Print << U"--- s3d::TOMLReader ---";
{
const TOMLReader toml{ U"test.toml" };
Print << (toml ? U"読み込み成功" : U"読み込み失敗"); // 失敗
}
Print << U"--- s3d::ext::TOML ---";
{
const ext::TOML toml = ext::TOML::Load(U"test.toml");
Print << (toml ? U"読み込み成功" : U"読み込み失敗"); // 成功
for (const auto& elem : toml[U"items"].arrayView())
{
if (elem.isInt())
{
Print << elem.get<int32>();
}
else if (elem.isString())
{
Print << elem.getString();
}
else if (elem.isBool())
{
Print << elem.get<bool>();
}
}
}
while (System::Update())
{
}
}
サンプル 2
テスト用TOML
toml.toml
# ===============================
# トップレベルのキーと各種スカラー
# ===============================
title = "TOML テストデータ"
# 整数(10進/16進/8進/2進・アンダースコア・符号)
answer = 42
neg-int = -1
plus-int = +99
hex-int = 0xDEAD_BEEF
oct-int = 0o755
bin-int = 0b1101_0101
zero = 0
# 浮動小数(通常/指数/アンダースコア/符号)
pi = 3.14159
planck = 6.626E-34
big-float = 1_000_000.000_001
small = -0.01
exp-pos = 5e+22
exp-neg = 1e-9
# 特殊な浮動小数(実装が対応していれば有効)
inf-pos = inf
inf-neg = -inf
not-a-num = nan
# 真偽値
ready = true
archived = false
# ===============================
# 文字列(基本/複数行/リテラル)
# ===============================
basic-str = "こんにちは \"TOML\" world\n改行テスト\tタブOK。Unicode: \u3053\u3093\u306B\u3061\u306F \U0001F680"
multiline-basic = """
複数行の"基本"文字列です。
末尾バックスラッシュで改行を無視します。\
ここは同じ行に連結されます。
"""
# リテラル(エスケープ解釈なし)
literal-str = 'Windowsパス C:\Users\name\Desktop\file.txt と バックスラッシュ \ はそのまま'
multiline-literal = '''
複数行の 'リテラル' 文字列。
"ダブルクォート" も \ バックスラッシュ もそのまま入ります。
'''
# 引用キー・日本語キー
"key with spaces" = "value"
'key.with.dots' = "ドットを含むキー(単一キー扱い)"
"日本語キー" = "OK"
# ===============================
# 日付/時刻系(RFC 3339/ローカル)
# ===============================
odt-utc = 1979-05-27T07:32:00Z
odt-offset = 1979-05-27T00:32:00+09:00
odt-fraction= 1979-05-27T07:32:00.999999Z
ldt = 1979-05-27T07:32:00 # Local Date-Time
ld = 1979-05-27 # Local Date
lt = 07:32:00 # Local Time
lt-fraction = 00:32:00.999999
# ===============================
# 配列(単一型・ネスト・末尾カンマ)
# ===============================
ints = [1, 2, 3, 4, 5,] # 末尾カンマ
strs = ["a", "b", "c"]
dates = [1979-05-27, 1980-01-01]
nested-ints = [[1, 2], [3, 4, 5]]
empty = [] # 空配列
# インラインテーブルとその配列
point = { x = 1, y = 2 }
name = { first = "John", last = "Smith" }
line = [ { x = 1, y = 2 }, { x = 2, y = 3 }, ]
empty-table = {} # 空のインラインテーブル
# ===============================
# ドット付きキー(暗黙テーブル生成)
# ===============================
site.name = "example"
site."owner name" = "Alice"
site.created = 2021-01-01
# ===============================
# 通常のテーブルとサブテーブル
# ===============================
[server]
ip = "192.168.1.1"
ports = [8001, 8001, 8002] # 値の重複はOK
enabled = true
[server.cache]
enabled = true
max_size = 1_024
# 引用キーを含むサブテーブル名
[servers]
[servers.alpha]
ip = "10.0.0.1"
role = "frontend"
[servers."beta.gamma"] # ドットを含むキー(引用で単一キー)
ip = "10.0.0.2"
role = "backend"
# ===============================
# 配列テーブル(オブジェクト配列)
# ===============================
[[product]]
name = "Hammer"
sku = 738594937
[[product]]
name = "Nail"
sku = 28475839
color = "gray"
# サブテーブルを含む配列テーブル要素
[[fruit]]
name = "apple"
[fruit.physical]
color = "red"
shape = "round"
[[fruit]]
name = "banana"
[fruit.physical]
color = "yellow"
shape = "curved"
コード
Main.cpp
# include <Siv3D.hpp>
# include "extTOML.hpp"
void TestCase(String name)
{
Console << U"==========================";
Console << U" " + name;
Console << U"==========================";
}
void ShowTable(const ext::TOML& root)
{
if (not root.hasValue())
{
return;
}
auto PrintTable = [&](auto&& self, const ext::TOML& table, int32 d) -> void
{
for (auto&& [key, value] : table.tableView())
{
const String indent((d * 2), U' ');
auto RecurseTable = [&](const ext::TOML& child)
{
Console << U"{}[{}]"_fmt(indent, key);
self(self, child, (d + 1));
};
if (value.isTable())
{
RecurseTable(value);
}
else if (value.isArray())
{
Console << U"{}{}:"_fmt(indent, key);
for (const auto& elem : value.arrayView())
{
if (elem.isTable())
{
RecurseTable(elem);
}
else
{
Console << U"{}{}"_fmt(indent, elem.format());
}
}
}
else
{
Console << U"{}{}: {}"_fmt(indent, key, value.format());
}
}
};
PrintTable(PrintTable, root, 0);
}
void Main()
{
{
{
const ext::TOML toml = ext::TOML::Load(U"toml.toml");
TestCase(U"format()");
Console << toml.format();
TestCase(U"get<int32>()");
Console << toml[U"answer"].get<int32>();
TestCase(U"get<double>()");
Console << toml[U"pi"].get<double>();
Console << toml[U"inf-pos"].get<double>();
TestCase(U"get<bool>()");
Console << toml[U"ready"].get<bool>();
TestCase(U"getString()");
Console << toml[U"basic-str"].getString();
TestCase(U"日本語キー");
Console << toml[U"日本語キー"].getString();
TestCase(U"odt-utc");
{
{
const auto [odt, offset] = toml[U"odt-utc"].getOffsetDateTime();
Console << odt.toDateTime();
Console << U"offset: " << offset << U" minutes";
}
{
const auto [odt, offset] = toml[U"odt-offset"].getOffsetDateTime();
Console << odt.toDateTime();
Console << U"offset: " << offset << U" minutes";
}
{
const auto [odt, offset] = toml[U"odt-fraction"].getOffsetDateTime();
Console << odt.toDateTime().format(U"yyyy-MM-dd HH:mm:ss.SSS");
Console << U"offset: " << offset << U" minutes";
}
}
TestCase(U"LocalDateTime");
{
const auto ldt = toml[U"ldt"].getLocalDateTime();
Console << ldt.toDateTime().format(U"yyyy-MM-dd HH:mm:ss.SSS");
}
TestCase(U"LocalDate");
{
const auto ld = toml[U"ld"].getLocalDate();
Console << ld.format(U"yyyy-MM-dd");
}
TestCase(U"LocalTime");
{
const auto lt = toml[U"lt"].getLocalTime();
Console << lt.toDateTime().format(U"HH:mm:ss.SSS");
}
TestCase(U"Array<int32>");
{
Console << toml[U"ints"].size();
Console << toml[U"ints"].getArray<int32>();
}
TestCase(U"Array<String>");
{
Console << toml[U"strs"].getArray<String>();
}
TestCase(U"Array<Date>");
{
Console << toml[U"dates"].getArray<Date>();
}
TestCase(U"Array<Array<int>>");
{
Console << toml[U"nested-ints"].getArray<Array<int32>>();
}
TestCase(U"空配列");
{
Console << toml[U"empty"].size();
Console << toml[U"empty"].isEmptyArray();
Console << toml[U"empty"].getArray<int32>();
Console << toml[U"empty"].getArray<String>();
Console << toml[U"empty"].getArray<ext::TOML>();
Console << toml[U"empty"].getArray<Array<int32>>();
}
TestCase(U"インラインテーブル");
{
{
const auto table = toml[U"point"];
Console << table.size();
Console << U"x: {}, y: {}"_fmt(table[U"x"].get<int32>(), table[U"y"].get<int32>());
}
{
Console << toml[U"name"].size();
Console << U"first: {}, last: {}"_fmt(
toml[U"name"][U"first"].get<String>(),
toml[U"name"][U"last"].get<String>());
}
}
TestCase(U"インラインテーブルの配列");
{
const auto lines = toml[U"line"].getArray<ext::TOML>();
Console << lines.size();
for (const auto& line : lines)
{
Console << U"x: {}, y: {}"_fmt(line[U"x"].get<int32>(), line[U"y"].get<int32>());
}
}
TestCase(U"空のインラインテーブル");
{
const auto emptyTable = toml[U"empty-table"];
Console << emptyTable.isTable();
Console << emptyTable.size();
}
TestCase(U"arrayView");
{
for (const auto& elem : toml[U"ints"].arrayView())
{
Console << elem.get<int32>();
}
}
TestCase(U"array of tables");
{
const auto tables = toml[U"product"].getArray<ext::TOML>();
Console << tables.size();
for (const auto& table : tables)
{
Console << U"name: {}, sku: {}"_fmt(
table[U"name"].get<String>(), table[U"sku"].get<int>());
}
}
TestCase(U"ShowTable");
{
ShowTable(toml);
}
}
TestCase(U"ShowTable (empty)");
{
ShowTable(ext::TOML{});
}
TestCase(U"isValid");
{
const ext::TOML toml = ext::TOML::Load(U"toml2.toml");
Console << (toml.isValid() == false);
}
TestCase(U"Parse");
{
const ext::TOML toml = ext::TOML::Parse(U"key = \"value\"\n"
"key2 = 123\n"
"key3 = true\n");
ShowTable(toml);
}
TestCase(U"Parse (comment only)");
{
const ext::TOML toml = ext::TOML::Parse(U"# This is a comment\n"
"# Another comment\n");
ShowTable(toml);
}
TestCase(U"Parse (empty)");
{
const ext::TOML toml = ext::TOML::Parse(U"");
ShowTable(toml);
}
TestCase(U"Parse (invalid)");
{
const ext::TOML toml = ext::TOML::Parse(U"a");
Console << (toml.isValid() == false);
ShowTable(toml);
}
}
while (System::Update())
{
}
}
ペンタブレットの筆圧情報を利用して毛筆ライクな線を描く
- ペンタブの筆圧を使って、毛筆風の太さ・にじみを表現するサンプル
- 左クリック(ペン先)を押している間は、移動量に応じて筆圧を補間しながら描画
- 動かさずに押し続けると「滲み
stayPressure
」でじわっと太る - 離した後は、圧が自然に抜ける余韻(減衰しながら進む)を描く
コード
# include <Siv3D.hpp>
class Pen
{
public:
/// @brief 現在の状態をデバッグ表示します
void show()
{
ClearPrint();
Print << (m_pos ? U"Pos: {:.1f}"_fmt(*m_pos) : U"Pos: None");
Print << U"Pressure: {:.3f}"_fmt(m_pressure);
Print << U"Direction: {:.2f}"_fmt(m_direction);
Print << U"StayPressure: {:.2f}"_fmt(m_stayPressure);
}
/// @brief 筆の状態を更新し、必要に応じて画像へ描画します
/// @param image 描き込み先の画像
/// @param deltaTime 前フレームからの経過時間(秒)
/// @return 描き込みが行われた場合 true, それ以外の場合は false
bool update(Image& image, const double deltaTime = Scene::DeltaTime())
{
// 目標値:現在の実筆圧
const double targetPressure = Pentablet::Pressure();
// 目標値:現在のカーソル位置
const Vec2 targetPos = Cursor::PosF();
// ペン(左ボタン)が押されている間
if (MouseL.pressed())
{
if (not m_pos) // 書き始め(前フレームまで未押下)
{
// 位置と筆圧を反映させる
m_pos = targetPos;
m_pressure = targetPressure;
// 描画
return paint(image);
}
// 前回の状態からの移動距離
const double moveDistance = m_pos->distanceFrom(targetPos);
if (moveDistance < 1.0) // ほぼ静止:にじみを増やす
{
// 筆圧のみ追従する
m_pressure = targetPressure;
// 静止時間に応じてにじみ成分を増加させる
m_stayPressure = Min((m_stayPressure + deltaTime), 1.0);
// 描画
return paint(image);
}
// にじみをリセットする
m_stayPressure = 0.0;
// 方向ベクトルを更新する
m_direction = (targetPos - *m_pos).normalized();
const double pressureDelta = ((targetPressure - m_pressure) / moveDistance);
bool updated = false;
// 1px 刻みで位置を進めて、その都度描画する
while (1.0 < m_pos->distanceFrom(targetPos))
{
// 筆圧を滑らかに変化させる
m_pressure += pressureDelta;
// 位置を 1px 進める
m_pos = Math::MoveTowards(*m_pos, targetPos, 1.0);
// 描画
updated |= paint(image);
}
return updated;
}
else // 離したあとの余韻(圧を徐々に抜きながら進む)
{
bool updated = false;
// 圧が十分小さくなるまで、方向へ少しずつ進めて描く
while (0.02 < m_pressure)
{
// 1ステップで少しずつ減衰
m_pressure = Math::MoveTowards(m_pressure, 0.0, 0.02);
// 最後の筆方向へ進める
*m_pos += m_direction;
// 描画
updated |= paint(image);
}
// 状態をリセットする
m_pos.reset();
m_pressure = m_stayPressure = 0.0;
return updated;
}
}
private:
/// @brief 実際の描き込みを行います。現在位置と筆圧から太さを決め、円ブラシで塗ります
/// @param image 描き込み先の画像
/// @return 描き込みが行われた場合 true, それ以外の場合は false
bool paint(Image& image)
{
if (not m_pos)
{
return false;
}
// 太さ:筆圧ベース + 静止によるにじみ分を加算する
const double thickness = ((m_pressure * 40.0) + (m_stayPressure * 5.0));
// わずかなオフセットを与えて重なりのムラを軽減する(テイスト付け)
const Vec2 center = (*m_pos + thickness * Vec2{ 1, 1 });
// 半透明の黒インクを重ね塗りする(にじみ感が出る)
Circle{ center, thickness }.paint(image, ColorF{ 0.0, 0.1 });
return true;
}
/// @brief 現在位置(未開始時は none)
Optional<Vec2> m_pos;
/// @brief 進行方向(正規化ベクトル)
Vec2 m_direction{ 0.0, 0.0 };
/// @brief 現在の筆圧(0.0~1.0)
double m_pressure = 0.0;
/// @brief 静止によるにじみ成分(0.0~1.0)
double m_stayPressure = 0.0;
};
void Main()
{
// ペンタブドライバが利用可能か確認する
if (not Pentablet::IsAvailable())
{
throw Error{ U"Pen tablet is not available" };
}
// キャンバスのサイズ
constexpr Size CanvasSize{ 1280, 720 };
// ウィンドウサイズをキャンバスに合わせる
Window::Resize(CanvasSize);
// 画像
Image image{ CanvasSize, Palette::White };
// 描いた結果を転送する先の動的テクスチャ
DynamicTexture texture{ image };
Pen pen;
while (System::Update())
{
// スペースキーでキャンバスをクリアする
if (KeySpace.down())
{
image.fill(Palette::White);
texture.fill(image);
}
// 状態が更新されたらテクスチャに反映する
if (pen.update(image))
{
texture.fill(image);
}
// 必要ならデバッグ表示
// pen.show();
// キャンバス描画
texture.draw();
}
}
曇っていくガラスを拭うエフェクト
- 元画像をガウシアンブラー+縮小/拡大でぼかし、曇りの色を付加した「曇りテクスチャ」を生成
- 拭う場合は、減算ブレンドにより曇りのアルファを削る
- 一定間隔で、加算ブレンドにより曇りが徐々に戻る
- カスタムシェーダ不要。
RenderTexture
の機能だけで実現可能
コード
# include <Siv3D.hpp>
/// @brief 曇った窓ガラスを拭うエフェクトを管理するクラス
class FoggedTexture
{
public:
/// @brief デフォルトコンストラクタ
[[nodiscard]]
FoggedTexture() = default;
/// @brief エフェクトを初期化します。
/// @param original 曇っていない元のテクスチャ
/// @param fogColor 曇りの色
[[nodiscard]]
explicit FoggedTexture(const Texture& original, const ColorF& fogColor)
: m_fogColor{ fogColor }
, m_original{ original }
, m_fogged{ MakeFogged(original, fogColor) } {}
/// @brief 元のテクスチャを返します。
/// @return 元のテクスチャ
[[nodiscard]]
const Texture& original() const noexcept
{
return m_original;
}
/// @brief 曇り表現のテクスチャを返します。
/// @return 曇り表現のテクスチャ
[[nodiscard]]
const RenderTexture& fogged() const noexcept
{
return m_fogged;
}
/// @brief 完全に曇った状態にリセットします。
void reset()
{
m_fogged = MakeFogged(m_original, m_fogColor);
}
/// @brief 指定した位置を拭います(曇りテクスチャのアルファを減らして透明にします)。
/// @param lastPos 前回拭った位置(クリックした瞬間は nullopt)
/// @param targetPos 今回拭う位置
/// @param wipeRadius 拭う半径
/// @param wipeStrength 拭う強さ(0.0 ~ 1.0)
void wipe(Optional<Vec2>& lastPos, const Vec2& targetPos, double wipeRadius, double wipeStrength)
{
const ColorF wipeColor{ 1.0, wipeStrength };
const ScopedRenderTarget2D target{ m_fogged };
// RGB は変えない。A は dstA - srcA で薄くなるようにする
const ScopedRenderStates2D blend{ AlphaSubtract() };
if (not lastPos)
{
// クリックした瞬間は単発の円
Circle{ targetPos, wipeRadius }.draw(wipeColor);
lastPos = targetPos;
}
else
{
// 高速にドラッグした場合の隙間を埋める
while (4.0 < lastPos->distanceFrom(targetPos))
{
lastPos = Math::MoveTowards(*lastPos, targetPos, 4.0);
Circle{ *lastPos, wipeRadius }.draw(wipeColor);
}
}
}
/// @brief 全体に曇りを追加します。
/// @param amount 曇りの追加量(0.0 ~ 1.0)
void addFog(double amount)
{
const ScopedRenderTarget2D target{ m_fogged };
const ScopedRenderStates2D blend{ AlphaAdd() };
Rect{ m_fogged.size() }.draw(ColorF{ 1.0, amount });
}
private:
/// @brief 曇りの色
ColorF m_fogColor{ 1.0, 0.7 };
/// @brief 元のテクスチャ
Texture m_original;
/// @brief 曇り表現のテクスチャ(拭いたり霧を足したりする描画対象)
RenderTexture m_fogged;
[[nodiscard]]
static void AddFog(const RenderTexture& texture, const ColorF& fogColor)
{
{
const ScopedRenderTarget2D target{ texture };
Rect{ texture.size() }.draw(fogColor);
}
Graphics2D::Flush();
}
[[nodiscard]]
static RenderTexture MakeFogged(const Texture& texture, const ColorF& fogColor)
{
const Size originalSize{ texture.size() };
const Size smallSize{ originalSize / 8 };
RenderTexture large1{ originalSize }, large2{ originalSize };
RenderTexture small1{ smallSize }, small2{ smallSize };
Shader::Copy(texture, large1);
Shader::GaussianBlur(large1, large2, large1);
Shader::Downsample(large1, small1);
Shader::GaussianBlur(small1, small2, small1);
Shader::Downsample(small1, large1);
AddFog(large1, fogColor);
return large1;
}
/// @brief 曇りの拭き取り用のブレンドステートを返します。
/// @return 曇りの拭き取り用のブレンドステート
[[nodiscard]]
static BlendState AlphaSubtract()
{
BlendState state = BlendState::Default2D;
state.src = Blend::Zero;
state.dst = Blend::One;
state.srcAlpha = Blend::SrcAlpha;
state.dstAlpha = Blend::One;
state.op = BlendOp::Add;
state.opAlpha = BlendOp::RevSubtract;
return state;
}
/// @brief 曇りの復帰用のブレンドステートを返します。
/// @return 曇りの復帰用のブレンドステート
[[nodiscard]]
static BlendState AlphaAdd()
{
BlendState state = AlphaSubtract();
state.opAlpha = BlendOp::Add;
return state;
}
};
void Main()
{
constexpr Size TargetSize{ 1280, 720 };
Window::Resize(TargetSize);
// 拭き取りの半径
const double WipeRadius = 100.0;
// 拭き取りの強さ(0.0 ~ 1.0)
const double WipeStrength = 0.1;
// 曇りの色
constexpr ColorF FogColor{ 1.0, 0.7 };
// 曇りテクスチャの作成
FoggedTexture foggedTexture{ Texture{ Image{ U"example/bay.jpg" }.scaled(TargetSize) }, FogColor };
// 最後に拭った位置
Optional<Vec2> lastPos;
// 曇りを追加する間隔(秒)と、その累積時間
constexpr double FogAddInterval = 0.04;
double fogAddAccumulatedTime = 0.0;
while (System::Update())
{
// 経過時間を加算
fogAddAccumulatedTime += Scene::DeltaTime();
// 曇りの追加時間に達したら曇りを追加
if (FogAddInterval < fogAddAccumulatedTime)
{
const int32 count = static_cast<int32>(fogAddAccumulatedTime / FogAddInterval);
foggedTexture.addFog(Math::Saturate(count * 0.1));
fogAddAccumulatedTime -= (count * FogAddInterval);
}
// 拭う
if (MouseL.pressed())
{
foggedTexture.wipe(lastPos, Cursor::PosF(), WipeRadius, WipeStrength);
}
else if (MouseL.up()) // 離したら最後の位置をリセット
{
lastPos.reset();
}
// 右クリックで拭き取りをリセット
if (MouseR.down())
{
foggedTexture.reset();
}
// 元画像を描く
foggedTexture.original().draw();
// その上に曇りテクスチャを描く
foggedTexture.fogged().draw();
}
}
自作クラスを JSON に対応させる
- 次の 2 つをする
-
Formatter
を定義してFormat
に対応させる -
operator>>
を定義してParse
に対応させる
-
コード
# include <Siv3D.hpp>
struct Position
{
int16 x = 0;
int16 y = 0;
// フォーマット可能にする
friend void Formatter(FormatData& formatData, const Position& value)
{
formatData.string += U"({}, {})"_fmt(value.x, value.y);
}
// パース可能にする
friend std::wistream& operator >>(std::wistream& is, Position& value)
{
wchar_t unused;
return is >> unused // '(' を読み飛ばす
>> value.x // x を読み取る
>> unused // ',' を読み飛ばす
>> value.y // y を読み取る
>> unused; // ')' を読み飛ばす
}
};
void Main()
{
const Position pos{ 100, 200 };
Print << pos;
Print << Parse<Position>(U"(300, 400)");
{
JSON json;
json[U"pos1"] = Position{ 11, 111 };
json[U"pos2"] = Position{ 22, 222 };
json.save(U"pos.json");
}
{
const JSON json = JSON::Load(U"pos.json");
const Position pos1 = json[U"pos1"].get<Position>();
const Position pos2 = json[U"pos2"].get<Position>();
Print << pos1;
Print << pos2;
}
while (System::Update())
{
}
}
原神 Web イベント『空月の歌』に登場する UI を再現する
- オリジナル Web サイト: https://genshin.hoyoverse.com/moon
コード
# include <Siv3D.hpp> // Siv3D v0.6.16
void DrawButton(const Circle& orbit, const double angleOnOrbit, const Mat3x3& homography, const std::pair<Texture, double>& icon, double& iconTime)
{
const Vec2 pos = orbit.getPointByAngle(angleOnOrbit);
double scale = homography.transformPoint(pos).distanceFrom(homography.transformPoint(pos.movedBy(1, 0)));
const Vec2 bottomCenter = homography.transformPoint(pos);
const Circle baseCircle{ Arg::bottomCenter = bottomCenter, 30 };
const double delta = Scene::DeltaTime();
if (baseCircle.mouseOver() || ((iconTime == 1.0) && Cursor::Delta().isZero()))
{
Cursor::RequestStyle(CursorStyle::Hand);
iconTime = (1.0 < iconTime) ? delta : Min((iconTime + delta * 1.5), 1.0);
}
else if (iconTime != 0.0)
{
iconTime = (2.0 <= iconTime) ? 0.0 : (iconTime + delta * 2);
}
double cs = scale;
double is = scale;
if (iconTime <= 1.0)
{
cs *= (1.0 + EaseOutBack(Math::Saturate(iconTime * 1.5)) * 0.25);
is *= (1.0 + EaseOutBack(iconTime * 1.2) * 0.5);
}
else
{
cs *= (1.0 + EaseInQuart(2.0 - iconTime) * 0.25);
is *= (1.0 + EaseInQuart(Math::Saturate(1.0 - (iconTime - 1.0) * 1.2)) * 0.5);
}
const Circle circle{ baseCircle.center, (baseCircle.r * cs) };
circle.draw(ColorF{ 0.02, 0.02, 0.05, 1.0 }, ColorF{ 0.02, 0.02, 0.05, 0.8 });
circle.drawFrame((0.8 * cs), ColorF{ 0.8 });
circle.scaled(0.95).drawFrame((0.4 * cs), ColorF{ 0.6 });
circle.scaled(0.90).drawFrame((0.3 * cs), ColorF{ 0.4 });
circle.scaled(0.85).drawFrame((0.2 * cs), ColorF{ 0.2 });
if ((0.0 < iconTime) && (iconTime <= 1.0))
{
circle.drawFrame(baseCircle.r * cs * 0.22, 0.0, ColorF{ 0.7, 0.5, 1.0, (0.2 * EaseOutQuart(iconTime)) });
const double e = EaseOutCirc(iconTime);
for (int32 i = 0; i < 4; ++i)
{
const double angle = (i * 90_deg) + (30_deg + e * 60_deg);
const Transformer2D tr{ Mat3x2::Translate(0, circle.r).rotated(angle).translated(circle.center) };
Shape2D::Astroid(Vec2{ 0, 0 }, (3 * cs), (5 * cs)).draw(ColorF{ 0.95, 0.95, 1.0 });
}
{
const double t = (0.5 - AbsDiff(0.5, iconTime));
const ScopedRenderStates2D add{ BlendState::Additive, SamplerState::ClampAniso };
circle.draw((ColorF{ 0.7, 0.5, 1.0 } *t), ColorF{ 0.0 });
}
}
else if (1.0 < iconTime)
{
const double t = (2.0 - iconTime);
circle.drawFrame(baseCircle.r * cs * 0.22, 0.0, ColorF{ 0.7, 0.5, 1.0, (0.2 * EaseInQuart(t)) });
for (int32 i = 0; i < 4; ++i)
{
const double angle = (i * 90_deg);
const Transformer2D tr{ Mat3x2::Translate(0, circle.r).rotated(angle).translated(circle.center) };
Shape2D::Astroid(Vec2{ 0, 0 }, (3 * cs * t), (5 * cs * t)).draw(ColorF{ 0.95, 0.95, 1.0 });
}
}
icon.first.scaled(0.45 * is).rotated(icon.second).drawAt(baseCircle.center, ColorF{ 1.0, Math::Saturate(0.5 * is) });
}
Vec2 GetPoint(const Vec2& wheelCenter, const int32 orbit, double angle, double ellipseRotationAngle)
{
if (orbit == 0)
{
return Circle{ wheelCenter, 480 }.getPointByAngle(angle);
}
else if (orbit == 1)
{
angle -= ellipseRotationAngle;
const Vec2 pointOnEllipse{ (wheelCenter.x + 540 * std::sin(angle)), (wheelCenter.y - 520 * std::cos(angle)) };
return Mat3x2::Rotate(ellipseRotationAngle, wheelCenter).transformPoint(pointOnEllipse);
}
else
{
return Circle{ wheelCenter, 598 }.getPointByAngle(angle);
}
}
void DrawCrossingLine(const Vec2& wheelCenter, const int32 fromOrbit, const double fromAngle, const int32 toOrbit, const double toAngle, double tAngle, const double ellipseRotationAngle)
{
Line{ GetPoint(wheelCenter, fromOrbit, (fromAngle + tAngle), ellipseRotationAngle), GetPoint(wheelCenter, toOrbit, (toAngle + tAngle), ellipseRotationAngle) }.draw(0.4);
}
void DrawCrossingBezier(const Vec2& wheelCenter, const int32 fromOrbit, const double fromAngle,
const int32 midOrbit, const double midAngle, const int32 toOrbit, const double toAngle, double tAngle, const double ellipseRotationAngle)
{
const Vec2 p0 = GetPoint(wheelCenter, fromOrbit, (fromAngle + tAngle), ellipseRotationAngle);
const Vec2 p1 = GetPoint(wheelCenter, midOrbit, (midAngle + tAngle), ellipseRotationAngle);
const Vec2 p2 = GetPoint(wheelCenter, toOrbit, (toAngle + tAngle), ellipseRotationAngle);
Bezier2{ p0, p1, p2 }.draw(0.4);
}
void Main()
{
Window::Resize(1280, 720);
const Size WheelTextureSize{ 1640, 1640 };
const Point WheelCenter{ WheelTextureSize / 2 };
const Size SmallWheelTextureSize{ 800, 800 };
const Point SmallWheelCenter{ SmallWheelTextureSize / 2 };
const MSRenderTexture renderTexture1{ WheelTextureSize, HasDepth::No, HasMipMap::Yes };
const MSRenderTexture renderTexture2{ SmallWheelTextureSize, HasDepth::No, HasMipMap::Yes };
const Array<std::pair<Texture, double>> icons{// { アイコン, 回転角度 }
{ Texture{ 0xF0A00_icon, 80 }, 0_deg }, // ライトキーパー | lighthouse-on
{ Texture{ 0xF0F2A_icon, 80 }, 0_deg }, // ファデュイ | snowflake-variant
{ Texture{ 0xF0C4D_icon, 80 }, 0_deg }, // 霜月の子 | car-tire-alert
{ Texture{ 0xF02F8_icon, 80 }, 0_deg }, // 雪国の妖精 | image-filter-vintage
{ Texture{ 0xF1C38_icon, 80 }, 180_deg }, // 宝盗団 | octagram-plus
{ Texture{ 0xF0EC8_icon, 80 }, 0_deg }, // ヴォイニッチ商会 | sail-boat
{ Texture{ 0xF18BE_icon, 80 }, 0_deg }, // 西風騎士団 | shield-sword
{ Texture{ 0xF0CF5_icon, 80 }, 0_deg }, // ワイルドハント | cross-celtic
{ Texture{ 0xF059F_icon, 80 }, -45_deg }, // 魔女会 | web
{ Texture{ 0xF0D14_icon, 80 }, 0_deg }, // カチャカチャ・クルムカケ工房 | heart-broken-outline
{ Texture{ 0xF1382_icon, 80 }, 0_deg }, // 冒険者協会 | compass-rose
};
Array<Vec3> stars;
while (stars.size() < 8000)
{
Vec2 pos = RandomVec2(Circle{ 820 });
const Circular c = pos;
const double d = c.r;
// 角度に応じた星の密度調整
if (d < 290)
{
if (InRange(c.theta, 20_deg, 135_deg) || (InRange(c.theta, 10_deg, 145_deg) && RandomBool(0.8)))
{
continue;
}
}
else if (d < 460.0)
{
if ((not InRange(c.theta, -135_deg, 20_deg)) || (not InRange(c.theta, -125_deg, 10_deg) && RandomBool(0.8)))
{
continue;
}
}
else
{
if (InRange(c.theta, -20_deg, 105_deg) || (InRange(c.theta, -10_deg, 115_deg) && RandomBool(0.8)))
{
continue;
}
}
// 軌道半径に応じた星の密度調整
if ((d < 110.0) || InRange(d, 220.0, 260.0) || InRange(d, 380.0, 430.0) || InRange(d, 550.0, 580.0) || InRange(d, 690.0, 730.0))
{
continue;
}
stars.emplace_back(pos.movedBy(WheelCenter), Random(0.2, 1.0));
}
Array<Vec4> backgroundStars;
for (int32 i = 0; i < 150; ++i)
{
const Vec2 pos = RandomVec2(Rect{ 1280, 720 });
const double size = Random(1.0, 3.0);
const double hue = Random(160.0, 420.0);
backgroundStars.emplace_back(pos, size, hue);
}
Array<double> iconTimes(icons.size());
while (System::Update())
{
const double t = Scene::Time();
const Vec2 mousePos = Cursor::PosF();
const double offset = Periodic::Sine1_1(18s);
const Quad quad1{ Vec2{ 150, 250 }, Vec2{ 1100, 40 }, Vec2{ (2200 + offset * 100), (480 + offset * 60) }, Vec2{ (-1000 + offset * 100), (1160 + offset * 60) } };
const Quad quad2 = Quad{ Vec2{ 820, 530 }, Vec2{ 1420, 390 }, Vec2{ (2170 + offset * 50), (680 + offset * 30) }, Vec2{ 20 + offset * 50, 1130 + offset * 30 } };
const Mat3x3 homography1 = Mat3x3::Homography(Rect{ WheelTextureSize }, quad1);
// 背景
{
// グラデーション
const Rect sceneRect = Scene::Rect();
Triangle{ sceneRect.tl(), sceneRect.tr(), sceneRect.bl() }.draw(ColorF{ 0.09, 0.07, 0.23 }, ColorF{ 0.02, 0.02, 0.04 }, ColorF{ 0.02, 0.02, 0.04 });
Triangle{ sceneRect.bl(), sceneRect.tr(), sceneRect.br() }.draw(ColorF{ 0.02, 0.02, 0.04 }, ColorF{ 0.02, 0.02, 0.04 }, ColorF{ 0.09, 0.07, 0.23 });
// 背景の水玉模様
for (int32 y = 0; y < 430; ++y)
{
for (int32 x = 0; x < 240; ++x)
{
if (IsEven(x + y))
{
Circle{ (x * 6 + 3), (y * 6 + 3), 2 }.draw(ColorF{ 0.02, 0.02, 0.05, 0.5 });
}
}
}
}
// レンダーテクスチャ 1(メインの歯車)
{
const ScopedRenderTarget2D target{ renderTexture1.clear(Palette::Black) };
// 軌道 1
{
Circle{ WheelCenter, 155 }.drawFrame(0.5);
Circle{ WheelCenter, 170 }.drawFrame(1);
Circle{ WheelCenter, 185 }.drawFrame(2);
}
// 軌道 2
{
{
const Circle circle{ WheelCenter, 290 };
circle.drawFrame(4);
// larege
{
const Transformer2D tr{ Mat3x2::Translate(0, circle.r).rotated(t * -6_deg - 125_deg).translated(WheelCenter) };
Shape2D::Astroid(Vec2{ 0, 0 }, 40, 58).draw();
Shape2D::Astroid(Vec2{ 0, 0 }, 28, 37).draw(Palette::Black);
}
{
const Transformer2D tr{ Mat3x2::Translate(0, circle.r).rotated(t * -6_deg).translated(WheelCenter) };
Shape2D::Astroid(Vec2{ 0, 0 }, 40, 58).draw();
Shape2D::Astroid(Vec2{ 0, 0 }, 28, 37).draw(Palette::Black);
}
{
const Transformer2D tr{ Mat3x2::Translate(0, circle.r).rotated(t * -6_deg + 125_deg).translated(WheelCenter) };
Shape2D::Astroid(Vec2{ 0, 0 }, 40, 58).draw();
Shape2D::Astroid(Vec2{ 0, 0 }, 28, 37).draw(Palette::Black);
}
// small
{
const Transformer2D tr{ Mat3x2::Translate(0, circle.r + 6).rotated(t * -6_deg - 90_deg).translated(WheelCenter) };
Shape2D::Astroid(Vec2{ 0, 0 }, 30, 54).draw(Palette::Black);
Shape2D::Astroid(Vec2{ 0, 0 }, 16, 34).draw();
}
{
const Transformer2D tr{ Mat3x2::Translate(0, circle.r + 6).rotated(t * -6_deg + 90_deg).translated(WheelCenter) };
Shape2D::Astroid(Vec2{ 0, 0 }, 30, 54).draw(Palette::Black);
Shape2D::Astroid(Vec2{ 0, 0 }, 16, 34).draw();
}
}
Circle{ WheelCenter, 302 }.drawFrame(0.5);
}
// 軌道 2.5
{
const Circle circle{ WheelCenter, 385 };
for (int32 i = 0; i < 240; ++i)
{
circle.getPointByAngle(i * 1.5_deg).asCircle(0.8).draw();
}
}
// 軌道 3
{
{
const Circle circle{ WheelCenter, 460 };
circle.drawFrame(1.5);
for (int32 i = 0; i < 24; ++i)
{
const Transformer2D tr{ Mat3x2::Translate(0, circle.r).rotated(t * 9_deg + i * 15_deg).translated(WheelCenter) };
Shape2D::Astroid(Vec2{ 0, 0 }, 8, 12).draw();
}
}
Circle{ WheelCenter, 470 }.drawFrame(0.5);
Circle{ WheelCenter, 480 }.drawFrame(0.25);
}
// 軌道 3.5
const double ellipseRotationAngle = (t * 6_deg);
{
const Transformer2D tr{ Mat3x2::Rotate(ellipseRotationAngle, WheelCenter) };
Ellipse{ WheelCenter, 540, 520 }.drawFrame(0.5);
}
// 軌道 3, 3.5, 4 をつなぐ線
{
DrawCrossingLine(WheelCenter, 1, 350_deg, 2, 350_deg, (t * 7_deg), ellipseRotationAngle);
DrawCrossingLine(WheelCenter, 0, 280_deg, 2, 285_deg, (t * 7_deg), ellipseRotationAngle);
DrawCrossingLine(WheelCenter, 0, 200_deg, 2, 200_deg, (t * 7_deg), ellipseRotationAngle);
DrawCrossingLine(WheelCenter, 0, 160_deg, 2, 160_deg, (t * 7_deg), ellipseRotationAngle);
DrawCrossingLine(WheelCenter, 0, 120_deg, 2, 120_deg, (t * 7_deg), ellipseRotationAngle);
DrawCrossingLine(WheelCenter, 1, 70_deg, 2, 70_deg, (t * 7_deg), ellipseRotationAngle);
DrawCrossingLine(WheelCenter, 1, 70_deg, 2, 60_deg, (t * 7_deg), ellipseRotationAngle);
DrawCrossingBezier(WheelCenter, 0, 300_deg, 1, 300_deg, 2, 340_deg, (t * 7_deg), ellipseRotationAngle);
DrawCrossingBezier(WheelCenter, 0, 120_deg, 1, 120_deg, 2, 90_deg, (t * 7_deg), ellipseRotationAngle);
DrawCrossingBezier(WheelCenter, 0, 180_deg, 1, 175_deg, 2, 185_deg, (t * 7_deg), ellipseRotationAngle);
DrawCrossingBezier(WheelCenter, 0, 270_deg, 1, 270_deg, 2, 240_deg, (t * 7_deg), ellipseRotationAngle);
DrawCrossingBezier(WheelCenter, 2, 0_deg, 1, 20_deg, 2, 40_deg, (t * 7_deg), ellipseRotationAngle);
DrawCrossingBezier(WheelCenter, 0, 240_deg, 2, 210_deg, 1, 200_deg, (t * 7_deg), ellipseRotationAngle);
}
// 軌道 4
{
Circle{ WheelCenter, 598 }.drawFrame(0.5);
Circle{ WheelCenter, 610 }.drawFrame(6);
Circle{ WheelCenter, 625 }.drawFrame(2.5);
Circle{ WheelCenter, 634 }.drawFrame(0.5);
}
// 軌道 5
{
Circle{ WheelCenter, 715 }.drawFrame(0.4);
Circle{ WheelCenter, 755 }.drawFrame(1);
{
const Circle circle{ WheelCenter, 770 };
circle.drawFrame(4);
for (int32 i = 0; i < 8; ++i)
{
const Transformer2D tr{ Mat3x2::Translate(0, circle.r).rotated(t * -5_deg + i * 45_deg).translated(WheelCenter) };
Shape2D::Astroid(Vec2{ 0, 0 }, 10, 18).draw();
}
}
}
// 月
{
const double outOffset = Periodic::Sine0_1(12s) * 30;
const Circle circle{ WheelCenter, (480 + outOffset) };
const Vec2 center = circle.getPointByAngle(77_deg);
center.asCircle(160).draw(Palette::Black);
center.asCircle(156).drawFrame(1);
center.asCircle(142).drawFrame(13);
center.asCircle(120).drawFrame(2.5);
center.asCircle(116).drawFrame(1);
{
const Circle inCircle{ center, 100 };
for (int32 i = 0; i < 90; ++i)
{
inCircle.getPointByAngle(i * 4_deg).asCircle(1).draw();
}
}
center.asCircle(86).draw();
center.movedBy(-13, -18).asCircle(62).draw(Palette::Black);
center.movedBy(-18, -20).asCircle(52).draw();
center.movedBy(-26, -24).asCircle(46).draw(Palette::Black);
}
}
// レンダーテクスチャ 2(右下の歯車)
{
const ScopedRenderTarget2D target{ renderTexture2.clear(Palette::Black) };
// 軌道 1
Circle{ SmallWheelCenter, 214 }.drawFrame(1);
// 軌道 2
{
const Circle circle{ SmallWheelCenter, 228 };
for (int32 i = 0; i < 240; ++i)
{
circle.getPointByAngle(i * 1.5_deg + t * -6_deg).asCircle(1.0).draw();
}
}
// 軌道 3 と 軌道 4 をつなぐ線
{
const Circle circle{ SmallWheelCenter, 280 };
for (int32 i = 0; i < 120; ++i)
{
const double angleSize = ((i % 5 == 0) ? 1.7_deg : 0.7_deg);
circle.drawArc(i * 3_deg + t * -6_deg - (angleSize * 0.5), angleSize, 30, 0, ColorF{ 0.25 });
}
}
// 軌道 3
Circle{ SmallWheelCenter, 250 }.drawFrame(3);
// 軌道 4
{
const Circle circle{ SmallWheelCenter, 280 };
circle.drawFrame(1.5);
for (int32 i = 0; i < 24; ++i)
{
const Transformer2D tr{ Mat3x2::Translate(0, circle.r).rotated(i * 15_deg + t * -6_deg).translated(SmallWheelCenter) };
Shape2D::Astroid(Vec2{ 0, 0 }, 7, 10).draw();
}
}
// 軌道 5
Circle{ SmallWheelCenter, 295 }.drawFrame(1);
}
// レンダーテクスチャへの描画完了処理
{
Graphics2D::Flush();
renderTexture1.resolve();
renderTexture1.generateMips();
renderTexture2.resolve();
renderTexture2.generateMips();
}
// 背景の星
for (const auto& star : backgroundStars)
{
const double s = std::sin(Math::Fraction(star.z * 100.0) * 2_pi + t * 0.25_pi);
const double shine = 1.0 - ((s + 1.0) * 0.4);
Circle{ star.xy(), (star.z * shine) }.draw(HSV{ star.w, 0.4, 1.0 });
}
// 星
{
const ScopedRenderStates2D add{ BlendState::Additive, SamplerState::ClampAniso };
for (const auto& star : stars)
{
Vec2 pos = homography1.transformPoint(star.xy());
const double r = ((homography1.transformPoint(star.xy().movedBy(1, 0)).distanceFrom(pos)) * star.z);
const double s = std::sin(Math::Fraction(star.z * 100.0) * 2_pi + t * 1_pi);
const double shine = 1.0 - ((s + 1.0) * 0.4);
// マウスカーソル周辺の星を押し出す
if (mousePos.distanceFrom(pos) < 70)
{
pos += (pos - mousePos).setLength((70 - mousePos.distanceFrom(pos)) * 0.4);
}
Circle{ pos, (r * 3.6) }.draw(ColorF{ 0.15, 0.2, 0.4, shine }, ColorF{ 0.01, 0.01, 0.02, shine });
Circle{ pos, (r * 0.75) }.draw(ColorF{ 0.4, 0.5, 0.8, shine });
}
}
// レンダーテクスチャを QuadWarp を用いて立体的に描画
{
const ScopedRenderStates2D add{ BlendState::Additive, SamplerState::ClampAniso };
{
const ScopedColorMul2D colorMul{ ColorF{ 1.0, (0.5 - Periodic::Sine0_1(6s) * 0.25) } };
Shader::QuadWarp(quad2, renderTexture2);
}
{
const ScopedColorMul2D colorMul{ ColorF{ 1.0, (1.0 - Periodic::Sine0_1(6s) * 0.5) } };
Shader::QuadWarp(quad1, renderTexture1);
}
}
// Y 軸の点線(Y-up)
{
const Vec2 center = homography1.transformPoint(WheelCenter);
const Vec2 begin{ (center.x - 90), (center.y - 360) };
const Vec2 dir = (center - begin).normalized();
for (int32 i = 0; i < 170; ++i)
{
Line{ Vec2{ begin + dir * (i * 5) }, Vec2{ begin + dir * (i * 5 + 2.5) } }.draw(0.25);
}
}
// X 軸の点線
{
const Vec2 center = homography1.transformPoint(WheelCenter);
const Vec2 begin{ (center.x - 660), (center.y + 140) };
const Vec2 dir = (center - begin).normalized();
for (int32 i = 0; i < 280; ++i)
{
Line{ Vec2{ begin + dir * (i * 5) }, Vec2{ begin + dir * (i * 5 + 2.5) } }.draw(0.3);
}
}
// Z 軸の点線
{
const double xOffset = (Periodic::Sine1_1(40s) * 300);
const Vec2 center = homography1.transformPoint(WheelCenter);
const Vec2 begin{ (center.x - 400 + xOffset), (center.y + 420) };
const Vec2 dir = (center - begin).normalized();
// 中心から手前への点線
for (int32 i = 0; i < 150; ++i)
{
const Vec2 pos{ center - dir * (i * 5) };
pos.asCircle(0.5).draw();
}
// 中心から奥への点線
for (int32 i = 0; i < 150; ++i)
{
const Vec2 pos{ center + dir * (i * 5) };
pos.asCircle(0.4).draw();
}
}
// ボタン
{
// 軌道 2
for (int32 i = 0; i < 3; ++i)
{
DrawButton(Circle{ WheelCenter, 270 }, (i * 120_deg + t * -6_deg), homography1, icons[i], iconTimes[i]);
}
// 軌道 4
for (int32 i = 0; i < 8; ++i)
{
DrawButton(Circle{ WheelCenter, 580 }, (i * 45_deg + t * -2_deg), homography1, icons[3 + i], iconTimes[3 + i]);
}
}
}
}
ミニゲームサンプル『Tornado Strike』
![]() |
![]() |
遊び方
-
マウスを動かして進路を決定!
- 左クリックで――ドーン!竜巻をぶっ放せ!
-
目指せ一撃で全壊!
- すべての家を一発で島の外へ吹き飛ばせばステージクリア!
-
油断大敵!
- 木の後ろにこっそり隠れている家を見落とさないように注意だ!
コード
# include <Siv3D.hpp>
struct Tornado
{
Bezier2 bezier;
Bezier2Path path;
double strength;
};
struct Building
{
int32 type = 0; // 0: 家, 1: 木
Vec2 origin{ 0, 0 };
Vec2 velocity{ 0, 0 };
Vec2 pos = origin;
};
Array<Building> GenerateBuildings()
{
Array<Building> buildings(6, Arg::generator = []() { return Building{ Random(0, 1), RandomVec2(Circle{ 500, 400, 190 }), Vec2{ 0, 0 } }; });
buildings.sort_by([](const auto& a, const auto& b) { return a.pos.y < b.pos.y; });
buildings[0].type = 0; // 少なくとも 1 つは家にする
return buildings;
}
void Main()
{
Window::SetTitle(U"Tornado Strike");
Window::Resize(600, 800);
constexpr Quad quad{ { 0,0 }, { 600, 0 }, { 1200, 800 }, { -600, 800 } };
constexpr double StepTime = 0.005;
const Font font{ FontMethod::MSDF, 48, Typeface::Heavy };
const Texture tornadoTexture{ U"🌪"_emoji };
const Array<Texture> buildingTextures{ Texture{ U"🏡"_emoji }, Texture{ U"🌳"_emoji } };
const MSRenderTexture renderTexture{ Size{ 1000, 1200 }, HasDepth::No, HasMipMap::Yes };
const Mat3x3 homography = Mat3x3::Homography(Rect{ renderTexture.size() }, quad);
Stopwatch resultStopwatch;
double accumulatedTime = 0.0;
int32 highScore = 0;
int32 score = 0;
Array<Building> buildings = GenerateBuildings();
Optional<Tornado> tornado;
while (System::Update())
{
const double t = Scene::Time();
const Vec2 tornadoOffset{ (Periodic::Triangle1_1(0.1s) * 2), (Periodic::Triangle1_1(0.13s) * 2) };
const Bezier2 bezier{ { 500, 1200 }, (Scene::Center() - (Scene::Center() - Cursor::Pos()) * 4), { 500, 380 } };
if (tornado)
{
accumulatedTime += Scene::DeltaTime();
while (StepTime <= accumulatedTime) // 竜巻の移動と建物への影響の計算
{
accumulatedTime -= StepTime;
tornado->path.advance(600.0 * StepTime);
tornado->strength -= (20.0 * StepTime);
const Vec2 tornadoPos = (tornado->bezier.getPos(tornado->path.getT()) + tornadoOffset);
for (auto& b : buildings)
{
if (b.type == 0) // 家に対してのみ影響
{
const Vec2 toTornado = (tornadoPos - b.pos);
if (const double distance = toTornado.length(); distance < 150.0)
{
const double force = ((1.0 - (distance / 150.0)) * tornado->strength * 400.0);
b.velocity += ((toTornado / distance) * force * StepTime);
}
b.pos += (b.velocity * StepTime);
b.velocity -= (b.velocity * (3 * StepTime));
}
}
}
if (not resultStopwatch.isStarted())
{
if (buildings.none([](const auto& b) { return (b.type == 0) && Circle { 500, 400, 240 }.contains(b.pos); })) // 成功
{
resultStopwatch.restart();
highScore = Max(++score, highScore);
}
else if (tornado->strength <= 0.0) // 失敗
{
resultStopwatch.restart();
score = 0;
}
}
else if (1.8s < resultStopwatch) // 次のステージへ
{
tornado.reset();
resultStopwatch.reset();
buildings = GenerateBuildings();
}
}
else
{
Cursor::RequestStyle(CursorStyle::Hand);
if (MouseL.down()) // 竜巻発射
{
tornado = Tornado{ bezier, Bezier2Path{ bezier }, 100.0 };
accumulatedTime = 0.0;
}
}
{
const ScopedRenderTarget2D target{ renderTexture.clear(ColorF{ 0.0, 0.4, 0.6 }) };
for (auto&& [x, y] : step({ 101, 121 })) // 海の描画
{
const double a = Math::Pow((std::sin((t + (std::sin(x / 4.0) / 2.0)) * 30_deg + y * 32_deg) * 0.5 + 0.5), 6);
Circle{ (x * 10), (y * 10), 5 }.draw(ColorF{ 0.1, 0.45, 0.65 }.lerp(ColorF{ 0.6, 0.7, 0.8 }, a));
}
font(score).drawAt(196, Vec2{ 500, 1096 }, ColorF{ 1.0, 0.4 });
font(U"HI-SCORE: {}"_fmt(highScore)).drawAt(20, Vec2{ 400, 1184 }, ColorF{ 1.0, 0.4 });
Circle{ 500, 400, 244 }.movedBy(0, Periodic::Sine1_1(4s) * 4).draw();
Circle{ 500, 400, 240 }.stretched(-2).draw(ColorF{ 0.9, 0.7, 0.4 }).stretched(-8, -12).draw(ColorF{ 0.4, 0.8, 0.2 }, ColorF{ 0.5, 0.8, 0.3 });
if (tornado)
{
tornado->bezier.draw(1); // 発射後の進路表示
}
else
{
bezier.draw(22, ColorF{ 1.0 }).draw(18, ColorF{ 0.9, 0.6, 0.7 }); // 発射前の進路表示
}
}
{
Graphics2D::Flush();
renderTexture.resolve();
renderTexture.generateMips();
const ScopedRenderStates2D sampler{ SamplerState::ClampAniso };
Shader::QuadWarp(quad, renderTexture);
}
const Vec2 tornadoPos = (tornado ? tornado->bezier.getPos(tornado->path.getT()) : Vec2{ 500, 1200 }) + tornadoOffset;
for (const auto& b : buildings)
{
const Vec2 pos = homography.transformPoint(b.pos);
const double scale = homography.transformPoint(b.pos).distanceFrom(homography.transformPoint(b.pos.movedBy(1, 0)));
if (b.type == 0) // 家の描画
{
buildingTextures[b.type].scaled(0.6 * scale).rotated(b.pos.distanceFrom(b.origin) * 2_deg).drawAt(pos);
}
else // 木の描画
{
const double force = Min(Math::Saturate(1.0 - ((tornadoPos - b.pos).length() / 120.0)) * (tornado ? tornado->strength : 0.0), 10.0);
const Transformer2D tr{ Mat3x2::Translate(0, -60).shearedX(Periodic::Sine1_1(0.2s) * force * 1_deg).translated(pos + Vec2{ 0, 60 }) };
buildingTextures[b.type].scaled(0.6 * scale).drawAt(Vec2{ 0,0 });
}
}
const double scale = homography.transformPoint(tornadoPos).distanceFrom(homography.transformPoint(tornadoPos.movedBy(1, 0)));
tornadoTexture.scaled(scale).draw(Arg::bottomCenter = homography.transformPoint(tornadoPos), ColorF{ 1.0, (tornado ? Math::Pow(tornado->strength / 100.0, 0.25) : 1.0) });
if (resultStopwatch.isRunning()) // 結果表示
{
const double s = resultStopwatch.sF();
const bool miss = (score == 0);
RectF{ (-4000 + s * 3000), 300, 4000, 200 }.draw(miss ? ColorF{ 0.3, 0.2, 0.5 } : ColorF{ 0.9, 0.6, 0.1 });
RectF{ (-4000 + s * 3000), 310, 4000, 6 }.draw().movedBy(0, 174).draw();
font(miss ? U"MISS!" : U"STRIKE!").drawAt((EaseOutBack(Min(s, 1.0)) * 100), Vec2{ 300, 400 }, ColorF{ 1.0, (s < 1.0 ? 1.0 : Max((1.4 - s) * 2.5, 0.0)) });
if (1.4 < s)
{
Scene::Rect().draw(ColorF{ 1.0, (s - 1.4) * 2.5 });
}
}
}
}