Windows PowerShell — 山内陽@portfolio\focus-areas\01
PS C:\Users\yamauchi\portfolio\focus-areas\01>

#

エンジンを使わずにDirectX 11で作ったゲーム。12人の3Dアクションを支えた土台。そしてUnreal Engineの上で、物理から組んだおもちゃ。

§01

WikiGOLF

PS> Get-Item .\WikiGOLF | Format-List *

Wikipediaの記事が、そのままゴルフコースになります。リンクがカップで、入った先の記事が次のコース。

WikiGOLF プレイ画面。地面に記事の本文が描かれている
地面に描かれているのは、記事の本文そのもの
7.3万行エンジンなしで書いたC++
399コミット
73自動テスト
  • 2026.01〜
  • 個人開発
  • C++20 / DirectX 11 / 自作ECS

1プレイ画面

W-2元になるWikipediaの記事をブラウザで開いた画面。見出し・表・リンクが一緒に見える範囲静止画 PNG → images/works/wikigolf/01-article.png
元になるWikipediaの記事「学校法人日本教育財団」(出典:ウィキペディア)
W-3W-2と同じ記事から生まれたコースを、真上から見た画面。W-2と同じ範囲が映るように静止画 PNG → images/works/wikigolf/02-course-top.png
同じ記事から生まれたコース。見出しは丘に、表と画像は台地に、リンクは旗の立つカップになります
W-4ショットを打ってから、ボールがリンクのカップに入り、リンク先の記事のコースに切り替わるまで動画 MP4(無音・5〜10秒) → images/works/wikigolf/03-cup-in.mp4
カップに入れた瞬間、そのリンク先の記事が次のコースになります
画面をもっと見る(4点)
W-9別々の記事から生まれた2つのコースを、左右に並べた画面動画 MP4(無音・5〜10秒) → images/works/wikigolf/08-compare.mp4
記事が違えば、コースも違う。左は「川口 (大阪市)」、右は「経済センサス」の記事から生まれたコースです
W-5ホールの始めに出る、スタートの記事と目的の記事の紹介画面静止画 PNG → images/works/wikigolf/04-intro.png
ホールの始めに、記事の説明とパーが出ます。パーは目的の記事までの最短の手数から決まります
W-6バンカーや氷の上など、ふつうの芝ではない地面にボールがある画面静止画 PNG → images/works/wikigolf/05-surface.png
地面の種類で、ボールの止まり方が変わります
W-7目的の記事にカップインしたあとのリザルト画面静止画 PNG → images/works/wikigolf/06-result.png
目的の記事に入れたら、たどった記事の道のりと打数が残ります

2作品の詳細

パーは、リンクの距離で決まる

パー = 目的の記事までの最短の手数 + 1

最短ルートを見つければパーが取れる、という分かりやすい目標にしました。日本語版Wikipediaの全記事のリンク関係をSQLiteに持ち、スタートと目的の両側から探索します(双方向の幅優先探索)。探索が長引いても、画面を閉じれば別のスレッドから sqlite3_interrupt() を呼んで止められます。

スタート記事から 出ていくリンクをたどる 目的の記事から 入ってくるリンクを逆にたどる スタート 目的の記事 ここで出会う 1 2 3 4
両側から広げると、片側だけで探すより調べる記事がずっと少なく済みます。この図では最短4手。目的の記事側から広げた結果は、目的地が同じあいだキャッシュして使い回します。

一人で作り続けるための仕組み

73

自動テスト

摩擦・弾道予測・パー計算・HTMLのレイアウトなどをCTestで確かめます。HTMLの処理はDirectXなしでも検証できるように切り分けました。

ImGui

デバッグツール

ボールの軌跡・速度・衝突履歴、Entity Inspector、フリーカメラ、コマ送り。表示はすべて日本語にしました。

CPU+GPU

プロファイラ

処理ごとの時間をCPUとGPUの両方で計り、CPU使用率やメモリと一緒にCSVで書き出します。

作品の詳細をもっと読む(遊び方・地面の種類)

遊び方

  1. 記事を読み込むスタートの記事から、地形とカップが生まれます
  2. 打つ9本のクラブと3回押しのゲージで狙います
  3. リンクに入れるカップインすると、リンク先の記事へ移動します
  4. 目的の記事へできるだけ少ない打数と移動回数でゴール

最初はピンボールとして作っていました。記事を壁にしてボールを弾く遊びでしたが、結果が偶然に左右されすぎました。狙ったリンクに意図して運べるゴルフの方が、リンクをたどる面白さに合っていたので作り直しました。

WikiGOLF マップビュー。記事の文字が載ったコースを真上から見ている
マップビュー。旗が立っている場所がリンク

地面の種類で、転がり方が変わる

転がるときの摩擦の倍率を地面ごとに変えています。バンカーは着地の勢いに応じてボールが砂に沈み込み、横に進む勢いも半分以下になります。水域と溶岩は、止まった時点でOB。

0.1 1 10 氷原 ×0.08 フェアウェイ ×1.0 グリーン ×1.45 ラフ ×2.5 バンカー ×7.5 氷原×0.08 フェアウェイ×1.0 ラフ×2.5 バンカー×7.5 グリーン×1.45 転がり摩擦の倍率(対数目盛り)

3こだわったこと

見えている文字と、足元の地形を、ずらさない

課題

地面には記事の本文がそのまま描かれていて、見出しの文字の上は丘に、リンクの文字の上にはカップがあります。以前の作り方では、見た目と地形が、同じ情報から作られていませんでした。

解決

記事をブラウザと同じようにレイアウトし、その1つの結果を、見た目と地形の両方に渡すようにしました。

1 取得 記事のHTML WinHTTPでWikipediaのAPIから 2 整形 本文・リンク・画像だけ残す Gumboで解析、固定のCSSを当てる 3 レイアウト litehtmlで配置を確定 文字の計測はDirectWrite 同じレイアウト結果を両方に使う 見た目:地面に貼るテクスチャ 高さ512pxのタイルを 1回に1枚ずつ描く 上限は3,200万画素。超えるときは 記事を切らずに解像度を下げる 地形:要素ごとに起伏を変える 見出し 画像・表 リンク 本文 盛り上がり 低い台地 平らなグリーンとカップ
取得・整形・レイアウトまでは1本道。その結果を、見た目と地形の両方が使います。
結果

文字の位置と、起伏・カップの位置がそろいました。記事を読みながら狙う場所を決める、という遊びがそのまま成り立つようになりました。

詳しい説明2行にまたがるリンクと、長い記事の描き方

以前の作り方では、記事の文字を均等に折り返して1枚の絵にし、地形は文字の量やキーワードから別に作っていました。見た目と地形が、同じ情報から作られていませんでした。

そのうえ記事は長く、画像や表も入ります。記事をまるごと1枚の絵にしようとすると、メモリも描く時間も足りません。

リンクは折り返しで2行にまたがることがあります。行ごとの矩形を全部覚えておき、いちばん大きい矩形の場所にカップを置きます。同じリンクは1つのカップにまとめます。

絵は高さ512pxずつに分けて、1回に1枚ずつ描きます。全体で3,200万画素を超えそうなときは、記事を途中で切らずに解像度を下げます。記事からうまく作れなかったときは、実行中に以前の作り方へ切り替えて、そのまま遊べるようにしています。

W-8低い位置から見た画面。見出しの文字の上が丘になり、リンクの文字の上に旗が立っているのが分かるもの静止画 PNG → images/works/wikigolf/07-text-and-terrain.png
地面の文字と、起伏・旗の位置が重なっています
つまずいたところロード画面が、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();

直したときのコミット:38514d0(2026年6月17日)

要になったコード描いた記事の四角形を、そのまま地形にする

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回のレイアウトの結果から作るので、あとからずれることがありません。表は縦に長くなりやすいので、バンカーにしています。

§02

フリップエスケイプ

PS> Get-Item .\FlipEscape | Format-List *

12人で作った3Dアクション。自分が作ったのはゲームの中身ではなく、12人が同時に作業できる「土台」と「進め方」。

フリップエスケイプ プレイ動画のサムネイル
プレイ動画(YouTubeで見る) | 紹介動画も見る
12人チーム
5ヶ月開発期間
536/ 1,089自分のコミット / チーム全体
  • 2025.10〜2026.02
  • リードプログラマ
  • C++17 / DirectX 11 / Effekseer / XAudio2

1プレイ画面

F-2チャージして弾かれ、壁に跳ね返りながらゴールに吸い込まれるまで動画 MP4(無音・5〜10秒) → images/works/flipescape/01-charge-flip.mp4
ためて、弾く。壁での跳ね返りを読んで、ゴールを目指します
暗いステージを上から見た画面。左上に残り時間
左上に残り時間。時間内にゴールへ向かいます
F-4ゴールに吸い込まれている瞬間か、クリアの画面静止画 PNG → images/works/flipescape/03-goal.png
ゴールに吸い込まれたらクリア
画面をもっと見る(1点)
F-3ステージ選択の画面動画 MP4(無音・5〜10秒) → images/works/flipescape/02-stage-select.mp4
ステージを選んで挑みます

2作品の詳細

担当の分け方

ゲームの中身と土台のあいだに、ビルダーAPIという境界を置きました。メンバーはこのAPIでオブジェクトを組み立てるだけで、DirectXのことを知らなくても作れます。

ゲームの中身チームメンバー
プレイヤーの挙動、ステージとステージ選択、ゲーム画面とタイトルのUI、サウンド
境界:ビルダーAPI
world.Create().With<Transform>(…).With<MeshRenderer>(…).Build()
土台自分
ECSWorld とコンポーネントの管理
描画影・発光・エフェクト・モデルの読み込み
入力XInput と DualSense、振動まで
開発の道具DebugDraw、設定のホットリロード

12人で進めるための仕組み

タスクはIssueのテンプレートから作ります。作業は3チームに分かれ、それぞれのブランチで進めます。担当が決まったときのDiscordへの通知と、masterの変更を各チームのブランチに取り込む処理は、GitHub Actionsで自動にしました。

master Team_A Team_B Team_C masterが更新されるたび、3チームへ自動で取り込む ビルドした成果物はZIPにしてGoogle Driveへ
取り込みは3チーム並列で行い、1チームで失敗しても他のチームは止めません。古い土台のまま作業が進むのを防ぐためです。
作品の詳細をもっと読む(土台として用意したもの・うまくいかなかったこと)

土台として用意したもの

DebugDraw

当たり判定やレイを画面に線で出します。ServiceLocator経由なので、どのコードからでも1行で呼べます。

ゲーム画面に当たり判定の枠が青い線で描かれている

SpatialHashGrid

空間をマス目に分け、同じマスと隣のマスにいる相手とだけ当たり判定をとります。オブジェクトが増えても、全部の組み合わせを調べずに済みます。

20個の組み合わせは190組。黄色の1個が調べるのは、周りの9マスにいる3個だけ。

うまくいかなかったこと

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 にその項目が自動で並びます。

コードでの宣言(CameraReaction.h)
ConfigVar<float> cfg_WallHitRumbleStrength{
                  "Camera.Reaction.WallHit",   // 設定ファイルの見出し
                  "WallHitRumbleStrength",     // 項目の名前
                  0.850f,                      // 最初の値
                  "壁衝突時振動強度" };          // 説明
自動でできる設定ファイル(config.toml)
[Camera.Reaction.WallHit]
              WallHitRumbleStrength = 0.85

ゲームはこのファイルの更新時刻を見ていて、保存されたら読み直します。ゲームを動かしたまま、値だけが変わります。配布するときは --bake を付けて起動し、config.bin にまとめます。

場面ごとの振動の強さと長さは、どれもこの設定ファイルで変えられます。今の値は次のとおりです。

チャージ解放 0.25–0.95 0.22s
壁に衝突 0.85 0.28s
タイムアップ 0.70 0.32s
ゴール吸引 0.35 0.24s
ステージ開始 0.32
00.51.0
強さ(0〜1)と長さ。現在の既定値。チャージ解放は、ためた量に応じて幅があります。
ノートPCで遊んでいる画面と、手に持ったDualSense
DualSenseで遊んでいるところ。弾く・ぶつかるの手応えは、コントローラーの振動でも返します
つまずいたところメモリを解放したら、次の日スカイボックスが消えた
起きたこと

タイトルやステージ選択に戻るたびに、ゲームのメモリ使用量が増えていました。これを直した次の日、今度はステージに入ると、背景の空(スカイボックス)が表示されなくなりました。

原因

メモリが増えていたのは、読み込んだモデルやテクスチャを、画面が切り替わっても解放していなかったからです。そこで、タイトルとステージ選択に入るたびに、テクスチャの管理クラスを作り直すようにしました。

ところが、ゲーム画面のシーンは起動時に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;
    }

直したときのコミット:ac8f827(メモリ、2026年1月28日)、5f54a77(スカイボックス、2026年1月29日)

要になったコード宣言した瞬間に、設定ファイルの項目になる

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行で済むのは、このコンストラクタのおかげです。変数ができた瞬間に、自分を設定の管理役へ登録します。

管理役は、登録された値の一覧から設定ファイルを書き出し、ファイルが保存されたら読み直します。値を足す人は、設定ファイルのことを考えなくて済みます。

§03

SLINKY

PS> Get-Item .\Slinky | Format-List *

バネのおもちゃ「スリンキー」で、終わらない階段をどこまでも下りていきます。第26回UE5ぷちコンで、応募377作品の中から技術賞をいただきました。

SLINKY 紹介動画のサムネイル。スリンキーが階段を下りていて、30コンボの表示が出ている
ぷちコンに応募した紹介動画(YouTubeで見る)
31物理でつないだ円盤
12効果音。すべてコードで合成
7,012行C++
  • 第26回UE5ぷちコン 技術賞
  • 個人開発
  • Unreal Engine 5.8 / C++

1プレイ画面

S-1スリンキーが階段を何段も続けて下りていき、コンボの数字が上がっていくところ動画 MP4(無音・5〜10秒) → images/works/slinky/01-descend.mp4
端をつかんで送り出すと、あとは自分で段を下りていきます
S-2標準の設定のスリンキー静止画 PNG → images/works/slinky/02-default.png
標準
S-3操作パネルで「コイル全長」と「最大伸び」を大きくしたスリンキー。S-2と同じ位置・同じ画角から静止画 PNG → images/works/slinky/03-stretchy.png
コイル全長と最大伸びを大きく
同じ位置から、値だけを変えた2本。13個の値を、遊びながら変えられます
会場の写真は、会期後に載せます

会場での展示

技術賞をいただいたことで、Unreal Fest Tokyo 2026(11月3〜4日、有明TFTビル)のUE5ぷちコンブースに、第25・26回の受賞作品と並んで展示されます。ぷちコンに提出したものと同じビルドを、会場で遊んでいただけます。

SLINKYの技術的な詳細を読む(設計・つまずき・コード)
S-4見た目を「虹色プラスチック」に切り替えたスリンキー静止画 PNG → images/works/slinky/04-rainbow.png
金属と虹色のプラスチックを切り替えられます。着地の音も変わります
S-5デイリーチャレンジを始めるときの画面(その日のプリセット名が見えるもの)静止画 PNG → images/works/slinky/05-daily.png
毎日、全員が同じスリンキーで挑みます
S-6ランキングの画面静止画 PNG → images/works/slinky/06-ranking.png
90秒で下りた段数が記録に残ります

2作品の詳細

遊び方

  1. 端をつかんで送り出すマウスでスリンキーの端をつかみ、下の段へ
  2. 続けて下りるとコンボ着地から1.4秒以内に次の段へ着けばつながります
  3. 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まで下げます。

輪がくっついているあいだは硬く、離れると柔らかく揺れます。本文の「伸びるほど、曲げのバネを弱める」を実際に行っているのが、この数行です。