牽引するゲームでは、自機が壁を避けるだけでは足りません。 私がCodexで制作している「灰都の救難艇」では、艇の後ろを動く救命カプセルと、その間のケーブルにも接触の判定を置きました。角を曲がるとき、艇は通れてもカプセルが建物へ引っかかるためです。
この記事は、v1で検証した牽引・接触・回収のCoreの作り方です。完成ゲームの画面や音を一から組む手順は扱いません。ゲームを直接遊ぶか、操作と制作意図を見ると、牽引の動きを確認できます。

動かせるC#サンプルで経路を比較する
.NET 10 SDKを用意し、Coreと検査操縦のZIPを展開します。追加NuGetパッケージは使いません。次をHaitoSample.csprojがある場所で実行します。
dotnet run --project HaitoSample.csproj -c Release
Wind route Won health 5 seconds 85.42043 damage 0 capsuleHealth 3,3,3 Wind no-wait comparison Lost health 5 rescued 0 seconds 9.600025 damage 3 capsuleHealth 0,3,3 Checks 69
出力のLostは検査の失敗ではありません。同じ任務へ、予告を待たない操縦を持ち込むと必須カプセルを失うことを確認しています。Program.csに成功する経路と比較対象の経路があり、Rescue.csに実ゲームの計算、RescueProgress.csに解放と自己ベストの保存規則があります。
v1.0.1では、保存記録の整合性の検査を1件追加しました。任務2のクリア記録が欠けているのに任務3の自己ベストだけ残る保存では、解放数を戻しても任務3の再訪ボタンが残っていました。解放範囲の確定後に範囲外の記録も除き、正常な任務1の自己ベストを保つ条件を確認しています。ZIPの末尾はこの追加を含むChecks 69です。牽引や敵の数値は変えていません。
艇・カプセル・ケーブルを分ける
手順 01
艇を動かす
壁と境界へ接触
手順 02
カプセルを引く
横風と角を検査
手順 03
ケーブルを検査
建物を横切るか
手順 04
回収する
位置・速度・時間
艇の半径は0.42、カプセルは0.48、ケーブルの長さは2.2です。接続中は通常速度を5.2から3.4へ落としました。艇から遠いカプセルを毎回同じ位置へ瞬間移動させるのではなく、ケーブルの長さを超えた分を引き、途中の建物へ接触するかも調べます。
V offset = c.Position - Ship;
if (offset.Length > CableLength)
{
V desired = Ship + offset.Unit * CableLength;
c.Position = Sweep(c.Position, desired - c.Position,
CapsuleRadius, out bool hit);
if (hit)
HurtCapsule(c, "カプセルが建物に接触。外側へ回るか切り離そう。");
V stretch = Ship - c.Position;
if (stretch.Length > CableLength + .1f)
{
Ship = Sweep(Ship, c.Position + stretch.Unit * CableLength - Ship,
ShipRadius, out _);
Velocity = new V();
}
}
カプセルが壁に止まった後も艇だけ進ませると、ケーブルが際限なく伸びます。そこで、艇の位置も最大長の近くまで制限します。接触状態からは、外側へ回る、切り離して退避する、拾い直すという選択を残しました。
接触による耐久減少には0.7秒の待ち時間を入れています。ただし無敵のまま抜けられるようにはせず、同じ壁際で押し続けるとカプセルは失われます。同じ接触状態から、押し続ける操縦は1.633秒、加速を押し続ける操縦は1.517秒で喪失しました。退避する操縦は再接続して回収できました。
加速中の薄い壁と、ケーブルだけの接触を検査する
Sweepは移動量を最大0.08ずつに分け、各位置で円と建物の矩形を検査します。斜め移動で全体が止まった場合はX、Yを順に試して壁沿いに滑らせます。この数値は本作の速度と地形で検証した値で、任意の薄壁を通り抜けない数学的保証とは扱いません。
int n = Math.Max(1, (int)Math.Ceiling(delta.Length / .08f));
V d = delta / n, p = from;
for (int i = 0; i < n; i++)
{
V full = p + d;
if (Free(full, radius)) { p = full; continue; }
contact = true;
V x = new V(p.X + d.X, p.Y);
if (Free(x, radius)) p = x;
V y = new V(p.X, p.Y + d.Y);
if (Free(y, radius)) p = y;
}
ケーブルは艇とカプセルの直線を0.08以下の間隔で調べ、半径0.025の接触を検査します。どちらの端も建物の外にいても、線が角を横切ればCableBlockedになります。接触中の艇の速度は0.35倍。ケーブルが建物の裏を通って問題なく引ける見た目を避けました。
別の記事との違い
艇だけの帰港を成功にしない
回収はカプセルが港の半径1.8以内、艇の速度が2.2以下、その状態が0.4秒続いた場合です。条件を外れたら滞在時間を0へ戻します。一度救助したものはRescuedにして再計上しません。
if (c.Health > 0 && (c.Position - Port).Length <= 1.8f
&& Velocity.Length <= 2.2f)
c.DockTime += dt;
else
c.DockTime = 0;
if (c.DockTime >= .4f)
{
c.Rescued = true;
Rescued++;
Attached = -1;
ReconnectTime = .8f;
}
最終回収とカプセル破壊の境界も検査しました。耐久0の必須カプセルを成功として数えず、すべての救助と艇の帰還がそろった場合にだけWonへ進みます。未達の帰港では、未回収の数や減速が必要な理由を画面に出します。
横風は「待つ意味があるか」まで比較した
横風を実装しても待つ意味がなかった
- 症状
- 最初の配置では、待たない大回りも無傷で成功した。
- 原因
- 風のある区間を短時間で通れ、予告を使う操縦と使わない操縦の結果が分かれなかった。
- 対処
- 風を遮らない低い架橋と、その狭い区間を組み直した。経路と周期を同条件で再実行し、別の救助順でも成功を確認した。
| 同じ横風任務の操縦方針 | 結果 | 時間 | カプセル耐久 |
|---|---|---|---|
| 予告を見て弱まる時間を待つ | 全員救助・帰還 | 85.42043秒 | 3 / 3 / 3 |
| 待たずに同じ経路を進む | 必須喪失・救助0 | 9.600025秒 | 0 / 3 / 3 |
| 中央のカプセルから先に拾う | 全員救助・帰還 | 85.42043秒 | 3 / 3 / 3 |
風の周期は静穏4秒、予告2秒、横風4秒、静穏2秒です。建物の風下には3.5の陰を作り、艇だけでなくカプセルの位置からも風を調べます。未接続のカプセルは固定具で保護し、出動後に待っているだけで目的物が壊れないようにしました。
風が動いて見えることと、風を読む必要があることは別々に確かめる。
数字は固定刻みの検査操縦の結果です。人が初めて遊んだときの難度や所要時間は測っていません。PCと375px幅のブラウザでは、実際のキーボード・マウス操縦パッド・タッチで全3任務の救助と保存を確認しました。スマホ幅はPC上のエミュレーションです。
まとめ
- 艇、カプセル、ケーブルの接触を分け、牽引物が止まったら艇の伸びすぎも制限する。
- 切り離しと再接続、減速と港内の滞在時間を設け、押し続けるだけの操作と立て直しの結果を比較する。
- 風のある任務は、予告を使う経路、使わない経路、別の救助順を実行して判断差を調べる。