1プレイ画面
#
エンジンを使わずにDirectX 11で作ったゲーム。12人の3Dアクションを支えた土台。そしてUnreal Engineの上で、物理から組んだおもちゃ。
WikiGOLF
PS> Get-Item .\WikiGOLF | Format-List *
Wikipediaの記事が、そのままゴルフコースになります。リンクがカップで、入った先の記事が次のコース。

2作品の詳細
パーは、リンクの距離で決まる
パー = 目的の記事までの最短の手数 + 1
最短ルートを見つければパーが取れる、という分かりやすい目標にしました。日本語版Wikipediaの全記事のリンク関係をSQLiteに持ち、スタートと目的の両側から探索します(双方向の幅優先探索)。探索が長引いても、画面を閉じれば別のスレッドから sqlite3_interrupt() を呼んで止められます。
一人で作り続けるための仕組み
73
自動テスト
摩擦・弾道予測・パー計算・HTMLのレイアウトなどをCTestで確かめます。HTMLの処理はDirectXなしでも検証できるように切り分けました。
ImGui
デバッグツール
ボールの軌跡・速度・衝突履歴、Entity Inspector、フリーカメラ、コマ送り。表示はすべて日本語にしました。
CPU+GPU
プロファイラ
処理ごとの時間をCPUとGPUの両方で計り、CPU使用率やメモリと一緒にCSVで書き出します。
作品の詳細をもっと読む(遊び方・地面の種類)
遊び方
- 記事を読み込むスタートの記事から、地形とカップが生まれます
- 打つ9本のクラブと3回押しのゲージで狙います
- リンクに入れるカップインすると、リンク先の記事へ移動します
- 目的の記事へできるだけ少ない打数と移動回数でゴール
最初はピンボールとして作っていました。記事を壁にしてボールを弾く遊びでしたが、結果が偶然に左右されすぎました。狙ったリンクに意図して運べるゴルフの方が、リンクをたどる面白さに合っていたので作り直しました。

地面の種類で、転がり方が変わる
転がるときの摩擦の倍率を地面ごとに変えています。バンカーは着地の勢いに応じてボールが砂に沈み込み、横に進む勢いも半分以下になります。水域と溶岩は、止まった時点でOB。
3こだわったこと
見えている文字と、足元の地形を、ずらさない
- 課題
-
地面には記事の本文がそのまま描かれていて、見出しの文字の上は丘に、リンクの文字の上にはカップがあります。以前の作り方では、見た目と地形が、同じ情報から作られていませんでした。
- 解決
-
記事をブラウザと同じようにレイアウトし、その1つの結果を、見た目と地形の両方に渡すようにしました。
取得・整形・レイアウトまでは1本道。その結果を、見た目と地形の両方が使います。 - 結果
-
文字の位置と、起伏・カップの位置がそろいました。記事を読みながら狙う場所を決める、という遊びがそのまま成り立つようになりました。
詳しい説明2行にまたがるリンクと、長い記事の描き方
以前の作り方では、記事の文字を均等に折り返して1枚の絵にし、地形は文字の量やキーワードから別に作っていました。見た目と地形が、同じ情報から作られていませんでした。
そのうえ記事は長く、画像や表も入ります。記事をまるごと1枚の絵にしようとすると、メモリも描く時間も足りません。
リンクは折り返しで2行にまたがることがあります。行ごとの矩形を全部覚えておき、いちばん大きい矩形の場所にカップを置きます。同じリンクは1つのカップにまとめます。
絵は高さ512pxずつに分けて、1回に1枚ずつ描きます。全体で3,200万画素を超えそうなときは、記事を途中で切らずに解像度を下げます。記事からうまく作れなかったときは、実行中に以前の作り方へ切り替えて、そのまま遊べるようにしています。
つまずいたところロード画面が、99%で固まる
- 起きたこと
記事を読み込むロード画面が、99%のあたりで数秒止まることがありました。止まっている間は、画面の演出もすべて止まります。
- 原因
描画スレッドが、別のスレッドで進む読み込みの完了を待っていました。
1つめは、演出の終わらせ方です。ロードが4秒を超えると、演出を先に終わらせていました。フェードが終わった時点で読み込みが済んでいないと、
future.get()が結果を待つあいだ、描画スレッドがブロックされます。2つめは、パーの計算です。記事を組み立てる最後の手順で、目的の記事までの最短の手数をデータベースから探していました。この探索も描画スレッドの上で動いていたので、リンクの多い記事ほど、描画が長く止まりました。
- 直し方
描画スレッドでは、待たないようにしました。毎フレーム
wait_for(0)で、完了したかどうかだけを確かめます。まだなら、演出を続けます。パーは、すぐに出せる仮の値で先に決めます。最短の手数は別のスレッドで探し、結果が届いたら、パーと旗の色を更新します。
src/game/scenes/LoadingScene.cpp(1080〜1084行目)
if (m_isLoading && m_loadTask.valid()) {
auto status = m_loadTask.wait_for(std::chrono::milliseconds(0));
if (status == std::future_status::ready) {
try {
m_loadedData = m_loadTask.get();
要になったコード描いた記事の四角形を、そのまま地形にする
src/game/systems/HtmlTerrainRegions.h(10〜16行目)
auto add=[&](const auto& r,HtmlRegionKind kind) {
out.push_back({r.x/texture.width,r.y/texture.height,r.width/texture.width,r.height/texture.height,kind});
};
for(const auto& r:texture.headings) add(r,HtmlRegionKind::Heading);
for(const auto& r:texture.images) add(r,HtmlRegionKind::Body);
// 表は縦長になりやすいため、他の要素と見分けが付くようバンカー(ハザード)として扱う。
for(const auto& r:texture.tables) add(r,HtmlRegionKind::Hazard);
地面に描いた記事の絵は、見出し・画像・表がどこにあるかを四角形で覚えています。それを絵の大きさで割って0〜1の割合に直し、そのまま地形を盛り上げる範囲にします。
見た目も地形も、同じ1回のレイアウトの結果から作るので、あとからずれることがありません。表は縦に長くなりやすいので、バンカーにしています。
フリップエスケイプ
PS> Get-Item .\FlipEscape | Format-List *
12人で作った3Dアクション。自分が作ったのはゲームの中身ではなく、12人が同時に作業できる「土台」と「進め方」。

担当リードプログラマ。ECS・描画・入力・開発の道具といった土台と、12人で進めるための仕組みを担当。
見どころ
1プレイ画面

画面をもっと見る(1点)
2作品の詳細
担当の分け方
ゲームの中身と土台のあいだに、ビルダーAPIという境界を置きました。メンバーはこのAPIでオブジェクトを組み立てるだけで、DirectXのことを知らなくても作れます。
world.Create().With<Transform>(…).With<MeshRenderer>(…).Build()12人で進めるための仕組み
タスクはIssueのテンプレートから作ります。作業は3チームに分かれ、それぞれのブランチで進めます。担当が決まったときのDiscordへの通知と、masterの変更を各チームのブランチに取り込む処理は、GitHub Actionsで自動にしました。
作品の詳細をもっと読む(土台として用意したもの・うまくいかなかったこと)
土台として用意したもの
DebugDraw
当たり判定やレイを画面に線で出します。ServiceLocator経由なので、どのコードからでも1行で呼べます。

SpatialHashGrid
空間をマス目に分け、同じマスと隣のマスにいる相手とだけ当たり判定をとります。オブジェクトが増えても、全部の組み合わせを調べずに済みます。
うまくいかなかったこと
APIを変えたことがチームに伝わらず、古い使い方のまま書かれたコードでビルドが壊れました。
その場では自分が直して統合しましたが、本当に必要だったのは、変更を全員が追える仕組みを先に作っておくことでした。今はPRとレビューで、変更の理由と影響範囲を残すようにしています。
3こだわったこと
手触りの調整で、ビルドを待たない
- 課題
-
振動や画面の揺れといった手触りは、実際に遊びながら何度も値を変えて決めたいところです。値を変えるたびにプログラムを直してビルドし直すと、1回の調整に何分もかかります。
- 解決
-
調整したい値は、コードの中で
ConfigVarとして1行で宣言し、設定ファイルconfig.tomlに自動で並ぶようにしました。ファイルが保存されたらゲームが読み直すので、ゲームを動かしたまま、値だけが変わります。 - 結果
-
場面ごとの振動の強さと長さは、どれもこの設定ファイルで変えられます。
F-5config.toml を書き換えて保存すると、動いているゲームの揺れや振動がすぐ変わる様子。エディタとゲームを横に並べて録画したもの動画 MP4(無音・5〜10秒) → images/works/flipescape/04-hot-reload.mp4保存した瞬間に、動いているゲームに反映されます
詳しい説明宣言のしかたと、いまの振動の値
値を変えるたびにプログラムを直してビルドし直すと、1回の調整に何分もかかります。12人で作っていると、その値がどこに書いてあるかを探すだけでも手間になります。
調整したい値は、コードの中で ConfigVar として1行で宣言します。すると、設定ファイル config.toml にその項目が自動で並びます。
ConfigVar<float> cfg_WallHitRumbleStrength{
"Camera.Reaction.WallHit", // 設定ファイルの見出し
"WallHitRumbleStrength", // 項目の名前
0.850f, // 最初の値
"壁衝突時振動強度" }; // 説明
[Camera.Reaction.WallHit]
WallHitRumbleStrength = 0.85
ゲームはこのファイルの更新時刻を見ていて、保存されたら読み直します。ゲームを動かしたまま、値だけが変わります。配布するときは --bake を付けて起動し、config.bin にまとめます。
場面ごとの振動の強さと長さは、どれもこの設定ファイルで変えられます。今の値は次のとおりです。

つまずいたところメモリを解放したら、次の日スカイボックスが消えた
- 起きたこと
タイトルやステージ選択に戻るたびに、ゲームのメモリ使用量が増えていました。これを直した次の日、今度はステージに入ると、背景の空(スカイボックス)が表示されなくなりました。
- 原因
メモリが増えていたのは、読み込んだモデルやテクスチャを、画面が切り替わっても解放していなかったからです。そこで、タイトルとステージ選択に入るたびに、テクスチャの管理クラスを作り直すようにしました。
ところが、ゲーム画面のシーンは起動時に1回だけ作り、ずっと使い回しています。そのシーンが、作り直す前のテクスチャを指すハンドル(識別番号)を持ったままでした。ハンドルがあるので「読み込み済み」と判断し、読み直していませんでした。
- 直し方
タイトルとステージ選択に入るときに、リソース・テクスチャ・キャッシュをまとめて解放します。ゲーム画面に入るときは、持っているハンドルを無効な値に戻してから、スカイボックスを作ります。
解放する側を直すなら、参照を持っている側も一緒に見直す必要がありました。
include/scenes/Game.h(312〜314行目、2792〜2795行目)
skyboxTexture_ = TextureManager::INVALID_TEXTURE;
skyboxTextureApplied_ = false;
CreateSkybox(world);
…
bool EnsureSkyboxTextureLoaded() {
if (skyboxTexture_ != TextureManager::INVALID_TEXTURE) {
return true;
}
要になったコード宣言した瞬間に、設定ファイルの項目になる
include/config/ConfigVar.h(24〜28行目)
ConfigVar(const std::string& section, const std::string& name, const T& defaultValue,
const std::string& comment = {})
: m_Section(section), m_Name(name), m_Value(defaultValue), m_DefaultValue(defaultValue), m_Comment(comment) {
ConfigManager::Instance().Register(this);
}
上の ConfigVar の宣言が1行で済むのは、このコンストラクタのおかげです。変数ができた瞬間に、自分を設定の管理役へ登録します。
管理役は、登録された値の一覧から設定ファイルを書き出し、ファイルが保存されたら読み直します。値を足す人は、設定ファイルのことを考えなくて済みます。
SLINKY
PS> Get-Item .\Slinky | Format-List *
バネのおもちゃ「スリンキー」で、終わらない階段をどこまでも下りていきます。第26回UE5ぷちコンで、応募377作品の中から技術賞をいただきました。

1プレイ画面
会場での展示
技術賞をいただいたことで、Unreal Fest Tokyo 2026(11月3〜4日、有明TFTビル)のUE5ぷちコンブースに、第25・26回の受賞作品と並んで展示されます。ぷちコンに提出したものと同じビルドを、会場で遊んでいただけます。
SLINKYの技術的な詳細を読む(設計・つまずき・コード)
2作品の詳細
遊び方
- 端をつかんで送り出すマウスでスリンキーの端をつかみ、下の段へ
- 続けて下りるとコンボ着地から1.4秒以内に次の段へ着けばつながります
- 90秒でどこまで行けるかチャレンジは下りた段数で記録が残ります
終わらない階段
階段は112段だけ用意し、スリンキーが通り過ぎた段を先へ回して使い回します。どれだけ下りても、置いてある段の数は変わりません。
プレイ中に段の高さや奥行きを変えると、ふつうは1段目を基準に全体が伸び縮みして、スリンキーの足元がずれます。そこで今いる段を基準にして、ほかの段をそこから並べ直すようにしました。
今日のスリンキー
毎日、バネと階段の値が変わります。サーバーは使わず、日付だけから値を決めるので、同じ日に遊ぶ人は全員同じスリンキーになります。日付からプリセットを1つ選び、値を±15%の範囲でずらします。
| プリセット | 標準から変わる値 |
|---|---|
| よく跳ねる日 | 反発係数 0.08→0.55 摩擦係数 0.48→0.22 減衰 1.0→0.7 |
| ふにゃふにゃの日 | 軸方向の硬さ 1.0→0.35 曲げの硬さ 1.0→0.3 減衰 1.0→0.6 |
| 太くて短い日 | コイル半径 45→75cm 線の太さ 1.25→3.0 コイル全長 180→80cm 巻き数 36→20回 |
| びよーんの日 | コイル全長 180→320cm 最大伸び 60→220cm 巻き数 36→55回 |
| 階段が険しい日 | 段の高さ 83→150cm 段の奥行き 100→70cm 摩擦係数 0.48→1.1 |
| 細かい階段の日 | 段の奥行き 100→35cm 段の高さ 83→30cm 蹴込み厚 10→4cm |
13個の値を全部ばらばらに決めると、遊べない組み合わせができてしまうため、プリセットを軸にしました。チャレンジ中は値を変える操作をすべて止めます。同じ条件で遊んでいなければ、端末内のランキングで記録を比べる意味がないためです。
配布版でだけ起きた不具合
エディタでは問題なく動くのに、配布用にビルドすると壊れました。原因は2つとも、ビルドの設定にありました。
| 症状 | 原因 | 対処 |
|---|---|---|
| 起動すると、真っ黒な画面のまま止まる | エディタでしか使わない設定を、配布版の起動時にも読みに行っていた | その設定をエディタ専用(bIsEditorOnly=True)にした |
| 「はじめから」を押しても、何も起きない | 名前で開くステージが、ビルドに含まれていなかった。UnrealPak -list で中身を見て確認 |
2つのステージを MapsToCook に明示した |
3こだわったこと
本物のスリンキーの、たわみ方を出す
- やりたかったこと
-
押し出すと、上の輪が弧を描いて次の段に移り、残りを引っぱって、自分で段を下りていきます。あの動きを、アニメーションではなく物理で出したいと考えました。
- ぶつかった壁
-
31枚の円盤を、同じ硬さの関節でつないだだけでは、2つの問題が出ました。自分の重さで潰れて、平たい円盤のようになってしまいます。反対に、伸ばすと棒のように硬いままで、柔らかく垂れません。
- どう作ったか
-
関節ごとに、硬さを変えました。
下の関節ほど硬くする。下の関節は、上に乗っている全部の重さを支えているためです。上の方は柔らかいままなので、つかんで引っぱりやすいです。
伸びるほど、曲げのバネを弱める。本物のスリンキーも、輪がくっついているあいだは曲がりにくく、離れると自由に揺れます。隣との間隔が伸びきりに近づくほど、曲げに逆らう力を弱めています。
隣どうし以外の円盤は、ぶつかり合う(当たり判定を持たせています)。縮んだときは、関節のバネだけでなく、輪どうしが実際に当たって自分を支えます。
- どうなったか
-
縮んでいるときは輪どうしで支え合い、段をまたぐときは弧を描いて、伸びたところは柔らかく垂れます。値を変えても同じ仕組みで動くので、13個の値をいじるだけで別のおもちゃになります。
S-7段をまたいで、弧を描いている瞬間を横から撮った画面静止画 PNG → images/works/slinky/07-arc.png段をまたぐ瞬間。上は伸びて、下は縮んでいます
要になったコード伸びるほど、曲げに逆らう力を弱める
Source/Slinky/SlinkyActor.cpp(872〜877行目)
const float StretchAlpha = FMath::Clamp(
(Distance - RestSpacing) / (MaximumNodeSpacing - RestSpacing), 0.0f, 1.0f);
const float SmoothStretch = FMath::SmoothStep(0.0f, 1.0f, StretchAlpha);
const float SwingLimit = FMath::Lerp(38.0f, 72.0f, SmoothStretch);
const float AngularStrength = FMath::Lerp(450.0f, 28.0f, SmoothStretch) * BendStiffnessScale;
const float AngularDamping = FMath::Lerp(22.0f, 4.0f, SmoothStretch) * DampingScale;
隣の円盤との距離が、ふだんの間隔から伸びきりまでのどこにあるかを0〜1で出し、SmoothStep でなめらかにします。その値で、曲げの強さを450から28まで下げます。
輪がくっついているあいだは硬く、離れると柔らかく揺れます。本文の「伸びるほど、曲げのバネを弱める」を実際に行っているのが、この数行です。