AIゲーム制作ラボ

Unityの連続衝突判定と入力リプレイの作り方:C#実例

星を渡るアクション「星継ぎ」のC#コードを配布。線分と円の最初の接触、予測線と衝突の共通化、120Hzの入力記録を再現します。1000ケースの端点判定との比較、3航路の実測、16検査のサンプル付きです。

Libra

公開 2026年9月21日

この記事の要点3項目

移動後の位置だけでは、円を横断したことを見落とす。 移動前後を結ぶ線分で最初の接触を求める。

残り2つの要点を読む
  • 予測線と実飛行は同じ判定を使う。 星も隕石も接触までの割合で比べ、最も手前を選ぶ。
  • 入力を描画フレームではなく120Hzのtickで保存。 配布コードで3航路と4つの描画頻度の再生を検査できる。

船が移動した線分と、星の捕捉円が最初に触れる場所を計算します。 Codexで作った「星継ぎ」は、星を回る船をタップで離陸させるアクションです。金色の予測線が着地点、赤い線が隕石への衝突を示します。

ここではゲーム本体のC#を使い、連続衝突判定と入力リプレイを動かします。完成画面を一から作る手順ではなく、航行計算をUnityから切り離して再現する手順です。

公開ゲームは6任務を追加し、開始画面も更新

「簡単すぎる、ステージの差が薄い」という指摘を受け、v1.1で燃料・集荷と帰路・鍵・開閉関門・崩れる軌道を組み合わせた6任務を追加しました。v1.2では開始画面を惑星と郵便船の絵・任務カード・出航ボタンへ作り直しています。v1.3では全任務が同じ6星だった配置を5〜10星の固有配置へ変更し、出航前の航路図と帰還条件の未達表示を追加しました。旧3航路は練習として残しています。このページのZIP・16検査・配信サイズは初版v1の再現資料で、新任務のルールは含みません。変更理由と比較は後半に追記しています。

星継ぎの隕石の回廊。赤い予測線が隕石へ続き、危険な航路と日本語で表示する
予測と実飛行は同じ判定を使う。赤い予測の方向へ離陸すると、実際に隕石へ衝突する。

配布C#を実行すると16検査を再現できる

.NET 10 SDKを用意し、星継ぎのC#サンプルZIPを展開します。HoshitsugiSample.csproj のあるフォルダで実行します。

dotnet run --project HoshitsugiSample.csproj

追加のNuGetパッケージはありません。ゲームv1のCoreコードをコピーし、独立した期待値を置いた Program.cs で検査しています。

サンプルの実行結果から抜粋成功
1000 crossings: segment=1000, endpoint=0
course 0: full=930 ticks/2533 points, shortcut=668 ticks/1816 points
PASS reject forged score
PASS replay at 30 Hz
PASS replay at 60 Hz
PASS replay at 120 Hz
PASS replay at 144 Hz
16 checks passed. No graphics or browser performance measured.
ファイル読む場所
Voyage.cs点・線分と円の判定、航行、予測、固定ステップの時計
Courses.cs3航路の星と隕石の位置
VoyageRecord.cs入力列を再生して記録の整合性を確認
Program.cs接線・静止・内側開始、航路比較、4頻度の再生検査

SDKが見つからなければ dotnet --list-sdks で10系を確認します。プロジェクトが見つからなければ展開先を確認。例外が出たら直前のPASSと Program.cs の条件を照合します。サンプルの16検査と、ゲーム本体のUnity57検査は別のものです。

終点だけを見ると、円の通り抜けを見落とす

円の半径を0.9とし、x=-2からx=2まで一度に動かします。終点は円の外でも、途中で円を横断しています。yを-0.899〜0.899へ変えた1000ケースを比べると、終点だけの判定は0件、線分の判定は1000件を検出しました。

これは横断するように作った境界ケースです。実ゲームの速度で1000回すり抜けた、という計測ではありません。星継ぎの通常移動は1tickあたり約0.0433単位なので、この比較とは移動距離が違います。

var from = new Point(-2, 0);
var to = new Point(2, 0);
bool hit = Sweep.Circle(from, to, new Point(0, 0), .9, out double t);
// hit=true、t=0.275。接触位置は from + (to-from) * t。

点Pを from + t * (to - from) と置き、円の中心からの距離が半径になるtを解きます。Sweep.Circle は小さい方の解を取り、0〜1の範囲なら線分内の接触として返します。

境界条件本実装の扱い
最初から円の内側t=0の接触
円の外で静止接触なし
円に接する線分接触あり
交点が線分の先・後ろ接触なし

船は点、星と隕石は静止円として扱っています。船の見た目の幅や、移動する隕石との相対運動は含めていません。自由形状の衝突へそのまま広げられるコードではありません。

飛び越しを許すため、全星から最初の接触を選ぶ

最初は次の番号の星へ渡る試作でした。私は「一つ飛ばしとかもっと先のやつに直接移動するのはダメなの?」と提案し、途中の星を任意にしました。目的地は星5。これで判定対象も「次の星だけ」から変わります。

FirstContact は出発星以外の全星と隕石を調べ、tが最小のものを採用します。番号が小さい星ではなく、飛行線分上で手前の接触が優先です。出発星を除外しないと、離陸直後にt=0で元の軌道へ捕まります。同時接触なら隕石を優先する規則にしました。

航路全配達の秒/点近道の秒/点
夜明けの便7.75/25335.57/1816
隕石の回廊11.96/24705.52/1817
星海の分岐12.13/24686.27/2056

中心付近を狙う機械操縦の実測です。近道は最初の2航路が3→5、最後が1→3→5。最適解や人のクリア時間ではありません。初回配達250点、到着500点、残り秒×15の切り捨てを組み合わせ、寄り道も得点になるようにしました。再訪で配達点は増えません。

金色の予測線と実飛行で同じ関数を使う

予測は接線方向へ最大2.5秒進んだ線分、実飛行は1tick分の線分です。長さは違っても、呼ぶ関数は FirstContact で揃えています。残り時間が短ければ予測も短くし、制限時間後の着地を約束しません。

01

金色になってから撮影を待つと、入力が間に合わなかった

症状
ブラウザ検査で金色の予測を確認し、撮影してからタップすると離陸に失敗した。
原因
撮影の間にも船は公転し、入力時には狙いが外れていた。
対処
新しく金色になる窓を待ってすぐ実入力し、撮影を着地後へ移した。内部メソッドで成功させる代わりに、実クリック・タップを維持した。

Unity検査では450位相から赤い予測と実衝突を照合し、星が隕石より手前なら星へ捕まる条件も確認しました。ブラウザでも実クリック・タップから隕石衝突まで通しています。

入力は120Hzのtickに記録して同じ順で再生する

航行は1/120秒ごとに Step を進めます。描画が60Hzなら通常は2回、30Hzなら4回分を進める時計です。入力は押し下げを一度だけ消費し、そのtickを記録します。描画フレーム番号や実時間のミリ秒を、そのまま再生入力には使いません。

次はサンプル内の再生と同じ考え方の抜粋です。inputs は単調増加のtick配列、v は開始済みの Voyage です。

bool launch = cursor < inputs.Length && inputs[cursor] == v.Ticks + 1;
if (launch) cursor++;
v.Step(launch);

30/60/120/144Hzで同じ入力列を適用し、終了tick・得点・位置が一致しました。ただし StepClock は1フレームの積算を0.1秒に制限します。長時間止まった画面の時間をすべて追いつかせる仕様ではありません。同じ入力の再生一致と、どんな負荷でも実時間通りに進むことは別です。

星継ぎの白い自己ベストゴーストが星1を回り、自機は出発星にいる画面
白い船は自己ベストの入力を同じtickで再生する。停止するとゴーストの時計も止まる。

保存した点数を信じず、入力から再計算する

記録には版・航路・終了tick・得点・入力列を保存します。読み込む前に入力数を512以下、tickを増加順かつ終了時刻以内に制限。VoyageRecord.Valid で最後まで再生し、成功・得点・終了tickが一致したものを採用します。

これで壊れた記録や得点だけ書き換えた記録を除外できます。ただしローカル記録の整合性確認であり、オンラインランキングの不正対策ではありません。ゲームのルールや星の位置を変える場合は保存版も分ける必要があります。

保存の検査確認した結果
再読み込み3航路の自己ベストと設定を復元
壊れたJSON・旧版日本語通知と記録除外で継続
書き込み拒否画面内の記録を残して継続
検査用の自動操縦通常の自己ベストを更新しない

localStorageとの接続はゲーム側に置き、配布サンプルには含めていません。サンプルで動かせる範囲は、保存前後の記録検証までです。

実機未確認の範囲も含めて検証結果を残す

UnityはEditMode51件、PlayMode6件が成功。表示ありChromeでPC幅と375px幅を使い、各3航路の近道・全配達・再生・保存復元を実入力で確認しました。停止・再開は全航路×両幅の追加6ケースでも確認しています。

ローカルWebGL計測結果と条件
圧縮済み配信4ファイル7,021,941 bytes
起動・PCの3回1011.7/636.9/624.6ms
起動・375px幅の3回682.1/648.9/588.1ms
描画間隔・各10秒6回とも中央値16.7ms、P95 16.8ms

キャッシュを無効にした新規ブラウザcontextで、隕石の回廊を測りました。375px幅も同じPCでのエミュレーションです。実機スマホの性能や公開サーバーからの通信時間は、この数字では分かりません。

完成した画面と操作は星継ぎの紹介ページにまとめています。Unityへつなぐ場合はCoreの3ファイルだけをAssetsへ置き、MonoBehaviourから StartStep を呼んで Position を描画する接続が必要です。描画・音・入力を含む制作全体の進め方はゲーム制作ガイドを参照してください。

v1.1:「簡単すぎる」を受け、着地と任務達成を分けた

公開後、私は「毎回簡単すぎる」「ステージの差もたいしてない」と指摘しました。そこで、金色なら即離陸するだけの操縦を測ると、旧3航路すべてで1080回1080回成功しました。入力を始める時刻を0〜1079tickへずらし、公転1周以上を含めた条件です。隕石があっても、予測線の指示に従えば攻略を変えずに済んでいました。

衝突が正しいことと、遊びに判断があることは別でした。 連打が失敗する検査と、解を知った操縦が成功する検査の間が抜けていました。制作ガイドにも技術の検証を重視する記述があったため、ステージごとの新しい判断と、単純な攻略の比較を両AI共通の公開条件へ加えました。

v1.1の任務必要になった判断
燃料三つの配達星2・4を経由し、3回の離陸で帰還する経路
帰り道の集荷星3の荷物を星1へ戻し、残る燃料で帰還
鍵の向こう側星2の鍵を先に取って星4の関門を通る
関門の時刻出発時ではなく、星4への到着時刻で開門を読む
一度きりの軌道必須星へ配達し、崩れた軌道へ戻らない順序
最後の往復便集荷・帰路・必須配達・崩壊・開門時刻の組合せ

金色は安全な着地点を示しますが、任務の成功までは保証しません。必須配達が終わっていない星5への着地はクリアにならず、燃料が尽きると終了します。予測を隠して操作だけを難しくする改修にはしませんでした。

v1.1では「金色なら即離陸」「前方の金色だけ」「番号順」の3戦略を各任務1080回ずつ試しました。金色追従では任務3が54回5%)、任務4が18回約1.7%)成功し、他の4任務は0回。番号順は全任務0回でした。一方、航路探索では6任務すべてのクリア入力列を確認しています。これは安直な攻略が通らなくなった測定で、人にとって十分難しい・面白いことの証明ではありません。初見の難易度は、引き続き別に確かめる必要があります。

最初の改修案では、帰路に約17〜42msしか押せない区間があり、タッチ検査で隕石に衝突しました。そこで任務専用の隕石配置を調整し、公転を0.75rad/秒、制限時間を120秒へ変更。検証経路では各離陸に約192ms以上の安全な窓を確認しました。成功率を無理に0へ下げるより、経路を考えた後に実際に操作できる猶予を優先しています。

v1.3:条件を増やすだけでは、帰還できない理由が伝わらなかった

続いて「星5まで行ったのにクリアしない」「全部同じ星数」「航路図がチープ」と指摘しました。コードでは必須配達や荷物の配達を検査していましたが、未達の帰還でも通常の着地と同じ表示でした。判定が仕様通りでも、理由が画面に出なければプレイヤーには不具合に見えます。

帰還港への未達着地では、未配達の星や未回収・未配送の荷物を表示して一時停止するよう変更。燃料が残っていれば続行でき、尽きた場合も未達内容を失敗理由に含めます。ゲーム内の目的地名も「帰還港」に統一しました。

同時に、出発地を含む6星の固定配置を、任務ごとに5・7・6・8・9・10星へ作り直しました。出航前の航路図には、星の位置と配達先・集荷・届け先・鍵・関門・帰還港を表示します。星数の追加だけでなく、往復や左右の分岐、長い帰還経路で寄る星を選ぶ構成です。

星継ぎの10星の航路図。集荷先、届け先、配達先と帰還港、帰還条件を表示
v1.3の出航前航路図。正解の経路は描かず、任務に必要な情報を示す。

この変更では、星の位置を変えると古い入力リプレイが成立しなくなる問題もありました。旧任務IDの配置と判定は残し、新任務には別IDを割り当てています。旧記録は内部に保管し、新配置の自己ベストには使いません。配布ZIPは初版の再現資料のままで、この拡張は含みません。

よくある質問

よくある質問

このサンプルだけでUnityのゲーム画面を作れますか?

再現できるのは航行・衝突・入力再生の計算です。描画、音、ブラウザ入力、localStorageとの接続は含みません。完成したゲームはページ冒頭の遊ぶリンクから試せます。

UnityのRigidbody2Dの衝突設定ですか?

今回は物理エンジンを使わず、点で表した船と静止円をC#で計算しています。Rigidbody2Dの設定や、回転する多角形への一般的な対処とは範囲が異なります。

120Hzで動かせば別の端末でも必ず同じになりますか?

固定tickだけでは保証できません。今回確認したのは同じコード・航路・入力列での終了tick、得点、位置の一致です。異なるCPUやランタイム間のビット単位の一致は検査していません。

ゴーストは最速の航路ですか?

最高得点の航路です。同点なら短時間を採用します。配達点があるため、最速の近道と最高得点の航路は一致しないことがあります。

出典

出典

  1. 01.NET 10 SDKのダウンロードMicrosoft2026-09-21にSDK 10.0.401の配布を確認。サンプルにはランタイムだけでなくSDKが必要。

この記事は役に立ちましたか?

関連記事