OpenSiv3Dで某アクションゲームのメニュー画面を再現したデモ
- READMEの「導入方法」に従いFlexLayoutをインストール
https://github.com/sthairno/FlexLayout/blob/main/README.md - Menu.xmlを
.exe
ファイルがあるディレクトリ(App/
ディレクトリ内)にコピー - ビルドして実行 🏃
OpenSiv3Dで某アクションゲームのメニュー画面を再現したデモ
.exe
ファイルがあるディレクトリ(App/
ディレクトリ内)にコピー#include <Siv3D.hpp> | |
#include <FlexLayout.hpp> | |
struct Item | |
{ | |
String emoji; | |
int level; | |
}; | |
void Main() | |
{ | |
constexpr double InspectorColorAlpha = 0.7; | |
Window::Resize(1280, 720); | |
// 画面表示用のアセット | |
const static Array<StringView> foodEmojis = { | |
U"🍇",U"🍈",U"🍉",U"🍊",U"🍋",U"🍌",U"🍍",U"🍎",U"🍏",U"🍐",U"🍑",U"🍒",U"🍓",U"🥝",U"🍅",U"🥥",U"🥑",U"🍆",U"🥔",U"🥕",U"🌽",U"🌶",U"🥒",U"🥦",U"🍄",U"🥜",U"🌰",U"🍞",U"🥐",U"🥖",U"🥨",U"🥞",U"🧀",U"🍖",U"🍗",U"🥩",U"🥓",U"🍔",U"🍟",U"🍕",U"🌭",U"🥪",U"🌮",U"🌯",U"🍳",U"🍲",U"🥣",U"🥗",U"🍿",U"🥫",U"🍱",U"🍘",U"🍙",U"🍚",U"🍛",U"🍜",U"🍝",U"🍠",U"🍢",U"🍣",U"🍤",U"🍥",U"🍡",U"🥟",U"🥠",U"🥡",U"🍦",U"🍧",U"🍨",U"🍩",U"🍪",U"🎂",U"🍰",U"🥧",U"🍫",U"🍬",U"🍭",U"🍮",U"🍯",U"🍼",U"🥛",U"☕",U"🍵",U"🍶",U"🍾",U"🍷",U"🍸",U"🍹",U"🍺" | |
}; | |
const Texture backgroundSrc{ U"example/bay.jpg" }; | |
const RenderTexture internalTexture{ backgroundSrc.size() }; | |
const RenderTexture backgroundTexture{ backgroundSrc.size() }; | |
Shader::GaussianBlur(backgroundSrc, internalTexture, backgroundTexture); | |
Texture playerTexture{ U"example/siv3d-kun.png" }; | |
FontAsset::Register(U"default", FontMethod::MSDF, 20, Typeface::Regular); | |
FontAsset::Register(U"mono-emoji", FontMethod::Bitmap, 30, Typeface::MonochromeEmoji); | |
FontAsset::Register(U"color-emoji", FontMethod::Bitmap, 70, Typeface::ColorEmoji); | |
Array<Item> items = | |
foodEmojis.choice(RandomClosed(10, 25)) | |
.map([](const StringView emoji) { | |
return Item{ | |
.emoji = String{ emoji }, | |
.level = RandomClosed(1, 29) | |
}; | |
}); | |
// テンプレート | |
const static auto CreateItemView = [](FlexLayout::Box& root, const Item& item) -> FlexLayout::Box { | |
auto view = root.getElementById(U"item-template")->cloneNode(true); | |
view.removeAttribute(U"id"); | |
auto emojiLabel = *view.getElementById(U"emoji-label")->asLabel(); | |
emojiLabel.setText(item.emoji); | |
auto levelLabel = *view.getElementById(U"level-label")->asLabel(); | |
levelLabel.setText(Format(item.level)); | |
return view; | |
}; | |
// レイアウト | |
Optional<FlexLayout::Box> main; | |
Optional<FlexLayout::Box> leftPanelHeader; | |
Array<FlexLayout::Label> leftPanelHeaderLabels; | |
Optional<FlexLayout::Box> leftPanelContent; | |
Array<FlexLayout::Box> itemViews; | |
Optional<FlexLayout::Label> heartLabel; | |
Optional<FlexLayout::Box> playerView; | |
Optional<FlexLayout::Box> footer; | |
Array<FlexLayout::Label> footerLabels; | |
// ホットリロードや初回ロードなど、ファイルを読み込んだあとに行う処理 | |
FlexLayout::Layout::OnLoadCallback onLoad = [&](FlexLayout::Layout&, FlexLayout::Box& root) { | |
// IDから要素を(再)取得 | |
main = root.getElementById(U"main"); | |
leftPanelHeader = root.getElementById(U"left-panel-header"); | |
leftPanelHeaderLabels = leftPanelHeader | |
->getElementsByClassName(U"header-item") | |
.map([](const auto& box) { return *box.asLabel(); }); | |
leftPanelContent = root.getElementById(U"left-panel-content"); | |
heartLabel = root.getElementById(U"heart-label")->asLabel(); | |
playerView = root.getElementById(U"player-view"); | |
footer = root.getElementById(U"footer"); | |
footerLabels = footer | |
->getElementsByClassName(U"footer-item") | |
.map([](const auto& box) { return *box.asLabel(); }); | |
// アイテムのビューを生成 | |
itemViews = items | |
.map([&](const Item& item) { | |
return CreateItemView(root, item); | |
}); | |
leftPanelContent->replaceChildren(itemViews); | |
// テクスチャのサイズを反映 | |
if (playerView) | |
{ | |
playerView->setStyle(U"width", playerTexture.width()); | |
playerView->setStyle(U"height", playerTexture.height()); | |
} | |
}; | |
FlexLayout::Layout layout{ U"Menu.xml", FlexLayout::EnableHotReload::Yes, onLoad }; | |
while (System::Update()) | |
{ | |
// 40秒周期で体力が増えたり減ったりするSiv3Dくん | |
size_t heartCount = static_cast<size_t>(Math::Round(Periodic::Triangle0_1(40s) * 20)); | |
heartLabel->setText(String(heartCount, U'❤')); | |
// レイアウトの更新 | |
layout.update(Scene::Rect()); | |
// ---背景--- | |
// ぼかし入り背景画像 | |
backgroundTexture.drawClipped({ 0, 0 }, *main->rect(), ColorF{ 0.5, 0.5, 0.5 }); | |
// ---左側パネル--- | |
// 角丸ヘッダー | |
leftPanelHeader->rect() | |
->rounded(10, 10, 0, 0) | |
.draw(Palette::Lemonchiffon); | |
// ヘッダーのアイコン | |
for (auto& label : leftPanelHeaderLabels) | |
{ | |
// XMLでactive属性が指定されている場合は色を濃くする | |
label.draw(label.hasAttribute(U"active") ? ColorF{ 0.1 } : ColorF{ 0.6 }); | |
} | |
// 半透明の黒背景 | |
leftPanelContent->rect() | |
->draw(ColorF{ 0.0, 0.0, 0.0, 0.5 }); | |
// アイテム | |
for (auto& itemView : itemViews) | |
{ | |
// 背景 | |
itemView.drawFrame(ColorF{ 0.4 }); | |
itemView.paddingAreaRect()->draw(ColorF{ 0.2 }); | |
// 絵文字 | |
auto emojiLabel = *itemView.getElementById(U"emoji-label")->asLabel(); | |
emojiLabel.draw(); | |
// レベル | |
auto levelLabel = *itemView.getElementById(U"level-label")->asLabel(); | |
levelLabel.drawFrame(ColorF{ 0.4 }); | |
levelLabel.paddingAreaRect()->draw(ColorF{ 0.1 }); | |
levelLabel.draw(); | |
} | |
// ---右側パネル--- | |
// ハート | |
heartLabel->draw(); | |
// プレイヤー (Siv3Dくん) | |
playerTexture.draw(playerView->rect()->pos); | |
// ---フッター--- | |
// 背景 | |
footer->rect()->draw(Palette::Lemonchiffon); | |
// ラベル | |
for (auto& label : footerLabels) | |
{ | |
label.draw(Palette::Black); | |
} | |
// ---デバッグ--- | |
if (auto hoveredBox = FlexLayout::Debugger::GetHoveredBox(*layout.document())) | |
{ | |
// カーソルを合わせているボックスのレイアウトを描画 | |
FlexLayout::Debugger::DrawLayout(*hoveredBox); | |
// 左クリックしたらConsoleにツリーを出力 | |
if (MouseL.down()) | |
{ | |
Console << U"--- Tree ---"; | |
Console << FlexLayout::Debugger::DumpTree(*hoveredBox); | |
} | |
} | |
} | |
} |