物理スライスパズルをモバイルUnityで破綻させない設計:LinecastNonAllocによる切断判定のゼロアロケーション化
ロープ切断やオブジェクトの物理スライスを主軸とするカジュアルパズルゲームを開発する際、Cut To Get Star Unity Complete Project のような完成プロジェクト資産は、2D物理ジョイントの連携、ステージ進行システム、星獲得ロジックの基盤があらかじめ組み込まれているため、ゲームメカニクスの検証期間を劇的に短縮できる。
【アーキテクチャの結論】
Unityによる物理スライスパズルにおいて、モバイル端末でのスワイプ操作時に微小なコマ落ち(マイクロスタッター)を根絶する核心は、「① Physics2D.LinecastAll などの配列新規確保APIを全廃し、事前確保バッファを用いた Physics2D.LinecastNonAlloc へ置き換えること、② 切断時の破片生成やエフェクトに Instantiate / Destroy を使わずオブジェクトプールで完全再利用すること、③ 毎フレームのタッチ軌跡サンプリングによるGC Alloc(マネージドヒープ確保)を完全にゼロへ抑え込むこと」 にある。切断の瞬間にガベージコレクション(GC)の停止時間を発生させない設計こそが、心地よいスライス体験を維持する絶対条件となる。
1. 現場の摩擦点:スワイプ切断判定によるGC Allocとフレームレートの乱れ
物理切断パズルゲームにおいて、プレイヤーの没入感を最も損なう要因は、「指でスワイプしてロープを切断したまさにその瞬間」に発生する画面のカクつきである。
多くのUnityテンプレート実装では、タッチ座標の移動検知(TouchPhase.Moved)ごとに、前フレームの座標と現在座標を結ぶ線分に対して Physics2D.LinecastAll() や Physics2D.RaycastAll() を安易に呼び出している。これらの標準APIは、ヒット結果を格納するための新しい RaycastHit2D[] 配列を毎フレームヒープメモリ上に生成(アロケーション)する。
スワイプ中は毎秒60回〜120回この判定が走るため、マネージドヒープは短時間でガベージで満杯になる。結果として、プレイヤーが爽快に切断操作を行った瞬間にUnityのガベージコレクタ(Boehm GC)が作動し、メインスレッドが10〜20ミリ秒停止して操作遅延やフレームドロップが直撃する。
2. 解決策:LinecastNonAllocと静的バッファによるゼロアロケーション実装
この描画負荷を完全に抑え込むには、ヒープ領域へのメモリ割り当てを一切行わない「NonAlloc」系APIを採用し、切断可能なジョイントやコライダーのみをビットマスクで絞り込んで判定する。
以下は、スワイプ切断処理におけるGC Allocを完全にゼロ化し、モバイル端末でも安定した60/120 FPSを担保するC#の実装コードである。
codeC#
using UnityEngine;
public class ZeroAllocSliceController : MonoBehaviour
{
[SerializeField] private LayerMask cuttableLayer;
[SerializeField] private int maxHits = 16;
private RaycastHit2D[] hitBuffer;
private Vector2 lastTouchPosition;
private bool isTouching;
private void Awake()
{
// 起動時に固定長のヒットバッファを事前確保(以降ヒープ割り当てなし)
hitBuffer = new RaycastHit2D[maxHits];
}
private void Update()
{
HandleTouchInput();
}
private void HandleTouchInput()
{
if (Input.touchCount == 0)
{
isTouching = false;
return;
}
Touch touch = Input.GetTouch(0);
Vector2 currentTouchPosition = Camera.main.ScreenToWorldPoint(touch.position);
if (touch.phase == TouchPhase.Began)
{
lastTouchPosition = currentTouchPosition;
isTouching = true;
}
else if (touch.phase == TouchPhase.Moved && isTouching)
{
// スワイプ移動距離が極小の場合は物理クエリをスキップ
if (Vector2.SqrMagnitude(currentTouchPosition - lastTouchPosition) > 0.01f)
{
ExecuteSlice(lastTouchPosition, currentTouchPosition);
lastTouchPosition = currentTouchPosition;
}
}
else if (touch.phase == TouchPhase.Ended || touch.phase == TouchPhase.Canceled)
{
isTouching = false;
}
}
private void ExecuteSlice(Vector2 startPoint, Vector2 endPoint)
{
// NonAlloc APIを使用し、事前確保したバッファに結果を上書き格納
int hitCount = Physics2D.LinecastNonAlloc(startPoint, endPoint, hitBuffer, cuttableLayer);
for (int i = 0; i < hitCount; i++)
{
RaycastHit2D hit = hitBuffer[i];
// 対象の切断コンポーネントを取得し、ジョイントを切断
if (hit.collider != null && hit.collider.TryGetComponent<ICuttableJoint>(out var cuttable))
{
cuttable.Cut(hit.point);
}
// バッファの参照をクリアして意図しない参照保持を防止
hitBuffer[i] = default;
}
}
}
public interface ICuttableJoint
{
void Cut(Vector2 cutPoint);
}
この実装では、ヒットバッファの再利用と TryGetComponent の活用により、スワイプ中の毎フレームのGC Allocが完全に 0 B に抑制される。切断対象のオブジェクト側でも Destroy() を直接呼ばず、ジョイントの enabled = false やオブジェクトプールへの返却を行うことで、物理演算エンジンの破綻を防ぐことができる。
3. Unity Profiler によるヒープ割り当ての自己診断
スライス処理の最適化が正しく機能しているかを確認するには、目視の感覚に頼らず、Unity Profilerを用いた客観的なデータ検証が必須である。
ゲームプレイループの挙動を検証するため、Cut To Get Star Unity Complete Project のソースプロジェクト を Unity エディタ上にインポートし、スワイプ判定のコールスタックをプロファイリングした。
開発環境で最適化の効果を検証する手順は以下の通りである:
-
Profiler ウィンドウを開く(Window > Analysis > Profiler)。
-
CPU Usage モジュールを選択し、表示モードを Hierarchy に切り替える。
-
GC Alloc 列でソートをかけ、スワイプ操作を実行中に ZeroAllocSliceController.Update 配下でアロケーション(バイト数)が発生していないか監視する。
診断の結果、ラインキャスト判定による毎フレームの GC Alloc が 0 B を維持し、長時間の連続スライス操作を行ってもガベージコレクションによるフレームドロップスパイクが一切観測されない健全な実行状態を確認できた。
4. Unity物理パズルゲームの必須アーキテクチャ監査リスト
モバイル向け物理パズルゲームをストアへリリースする前に、以下の5項目を設計監査基準として確認することを推奨する:
物理クエリのNonAlloc化:LinecastAll、RaycastAll、OverlapCircleAll 等の配列返却APIを全廃し、NonAlloc 派生APIへ置換されているか?
スワイプ追従時のGC Allocゼロ化:Unity Profilerの CPU Usage において、スワイプ中に入力処理由来のGCアロケーションが完全に 0 B に抑えられているか?
物理ステップと入力サンプリングの整合性:タッチ入力は Update で高速サンプリングしつつ、ジョイント切断後のRigidbody挙動への反映が FixedUpdate のタイムステップと矛盾していないか?
切断破片のオブジェクトプーリング:切断エフェクト、ロープ破片、星パーティクルが Instantiate / Destroy されず、事前生成プールから使い回されているか?
レイヤーマスクによるクエリの局所化:LinecastNonAlloc に渡すレイヤーマスクが適切に設定され、背景やUIコライダーなど切断対象外のオブジェクトを早期除外できているか?