Unityを触り始めたころは、つい何でもかんでも Update() に書いてしまいがちですよね。移動も、入力も、エフェクトも、ゲーム全体の状態管理も、ぜんぶひとつのスクリプトに押し込んでしまう…。
その場では動くのですが、少し仕様が変わるだけでコードがぐちゃぐちゃになり、「どこを直せばいいのか分からない」状態になりがちです。
とくに「時間停止」みたいなゲーム全体に影響するギミックを、巨大なGodクラスの Update() にねじ込むと、
- プレイヤーだけ止める / 敵だけ止める / 弾だけ止める…の条件分岐だらけになる
- 新しいオブジェクトを追加するたびに、その巨大クラスを編集しないといけない
- バグが出たときに、どの処理が悪さをしているのか追うのが大変
そこでこの記事では、「時間停止」という機能をひとつの責務に切り出した小さなコンポーネントとして実装していきます。
今回作る 「TimeStop」コンポーネント は、発動中、自分以外のすべてのオブジェクトの物理挙動(Rigidbody)を止める ことに専念させます。
【Unity】世界を止めて自分だけ動く!「TimeStop」コンポーネント
ここでは Unity6(C#)で動く、コピペ可能なフルコードを紹介します。
ポイントは以下の通りです。
- 「時間停止中かどうか」を管理するのは TimeStop コンポーネントだけ
- 停止対象は
Rigidbody/Rigidbody2Dを持つオブジェクト - 自分自身(TimeStop を持つオブジェクト)の物理挙動は止めない
- 時間停止の ON / OFF をトグルできる
- 停止前の速度・角速度を記録して、解除時に復元する
フルコード
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem; // 新InputSystemを使う場合
/// <summary>
/// 時間停止コンポーネント。
/// 発動中は「自分以外」の Rigidbody / Rigidbody2D の動きを止める。
/// ・TimeStopコンポーネントを持つオブジェクトは動き続ける
/// ・停止前の速度・角速度を保存して、解除時に復元する
/// ・InputAction でトグル発動も可能
/// </summary>
public class TimeStop : MonoBehaviour
{
[Header("入力設定")]
[SerializeField]
private bool useInputAction = true;
// true: InputAction を使ってトグル
// false: スクリプトや他コンポーネントから手動で呼ぶ
[SerializeField]
private InputActionReference toggleAction;
// InputSystem の Action をアサイン(例: Keyboard T キー)
[Header("検索設定")]
[SerializeField]
private bool includeInactiveObjects = false;
// true: 非アクティブなオブジェクトも対象に含める(通常は false 推奨)
[SerializeField]
private bool searchEveryToggle = false;
// true: トグルのたびにシーン内の Rigidbody を検索し直す
// false: Start 時に一度だけ検索してキャッシュ(動的生成が多いなら true も検討)
[Header("デバッグ表示")]
[SerializeField]
private bool logDebugInfo = false;
/// <summary>現在時間停止中かどうか</summary>
public bool IsTimeStopped { get; private set; }
/// <summary>停止対象の Rigidbody とその元の速度情報</summary>
private readonly Dictionary<Rigidbody, RigidbodyState> rigidbodyStates =
new Dictionary<Rigidbody, RigidbodyState>();
/// <summary>停止対象の Rigidbody2D とその元の速度情報</summary>
private readonly Dictionary<Rigidbody2D, Rigidbody2DState> rigidbody2DStates =
new Dictionary<Rigidbody2D, Rigidbody2DState>();
/// <summary>停止前の 3D Rigidbody の状態</summary>
private struct RigidbodyState
{
public Vector3 velocity;
public Vector3 angularVelocity;
public RigidbodyConstraints constraints;
public bool isKinematic;
}
/// <summary>停止前の 2D Rigidbody の状態</summary>
private struct Rigidbody2DState
{
public Vector2 velocity;
public float angularVelocity;
public RigidbodyConstraints2D constraints;
public bool simulated;
}
private void Awake()
{
// 入力を使う設定なら、Action にコールバックを登録
if (useInputAction && toggleAction != null)
{
toggleAction.action.performed += OnTogglePerformed;
}
}
private void OnEnable()
{
if (useInputAction && toggleAction != null)
{
toggleAction.action.Enable();
}
// キャッシュ方式の場合は最初に一度だけスキャン
if (!searchEveryToggle)
{
CacheAllRigidbodies();
}
}
private void OnDisable()
{
if (useInputAction && toggleAction != null)
{
toggleAction.action.Disable();
}
// 無効化時に時間停止中であれば、必ず解除しておく
if (IsTimeStopped)
{
ResumeTime();
}
}
private void OnDestroy()
{
if (useInputAction && toggleAction != null)
{
toggleAction.action.performed -= OnTogglePerformed;
}
}
/// <summary>
/// 入力Actionによるトグルイベント
/// </summary>
/// <param name="ctx"></param>
private void OnTogglePerformed(InputAction.CallbackContext ctx)
{
ToggleTime();
}
/// <summary>
/// 外部から呼び出せるトグル関数。
/// </summary>
public void ToggleTime()
{
if (IsTimeStopped)
{
ResumeTime();
}
else
{
StopTime();
}
}
/// <summary>
/// 時間を停止する。
/// </summary>
public void StopTime()
{
if (IsTimeStopped)
{
return;
}
if (searchEveryToggle)
{
CacheAllRigidbodies();
}
// すべての Rigidbody を停止状態に変更
foreach (var kvp in rigidbodyStates)
{
Rigidbody rb = kvp.Key;
if (rb == null) continue;
// 速度を保存
RigidbodyState state = kvp.Value;
state.velocity = rb.velocity;
state.angularVelocity = rb.angularVelocity;
// 物理挙動の設定を保存
state.constraints = rb.constraints;
state.isKinematic = rb.isKinematic;
rigidbodyStates[rb] = state;
// 実際に止める
rb.velocity = Vector3.zero;
rb.angularVelocity = Vector3.zero;
rb.isKinematic = true; // 物理シミュレーションを止める
}
foreach (var kvp in rigidbody2DStates)
{
Rigidbody2D rb2d = kvp.Key;
if (rb2d == null) continue;
Rigidbody2DState state = kvp.Value;
state.velocity = rb2d.velocity;
state.angularVelocity = rb2d.angularVelocity;
state.constraints = rb2d.constraints;
state.simulated = rb2d.simulated;
rigidbody2DStates[rb2d] = state;
rb2d.velocity = Vector2.zero;
rb2d.angularVelocity = 0f;
rb2d.simulated = false; // 2D物理シミュレーションを止める
}
IsTimeStopped = true;
if (logDebugInfo)
{
Debug.Log($"[TimeStop] 時間停止開始: 対象 Rigidbody = {rigidbodyStates.Count}, Rigidbody2D = {rigidbody2DStates.Count}");
}
}
/// <summary>
/// 時間停止を解除する。
/// </summary>
public void ResumeTime()
{
if (!IsTimeStopped)
{
return;
}
// 停止前の状態を復元
foreach (var kvp in rigidbodyStates)
{
Rigidbody rb = kvp.Key;
if (rb == null) continue;
RigidbodyState state = kvp.Value;
rb.isKinematic = state.isKinematic;
rb.constraints = state.constraints;
rb.velocity = state.velocity;
rb.angularVelocity = state.angularVelocity;
}
foreach (var kvp in rigidbody2DStates)
{
Rigidbody2D rb2d = kvp.Key;
if (rb2d == null) continue;
Rigidbody2DState state = kvp.Value;
rb2d.simulated = state.simulated;
rb2d.constraints = state.constraints;
rb2d.velocity = state.velocity;
rb2d.angularVelocity = state.angularVelocity;
}
IsTimeStopped = false;
if (logDebugInfo)
{
Debug.Log("[TimeStop] 時間停止解除");
}
}
/// <summary>
/// シーン内の Rigidbody / Rigidbody2D を検索して、辞書にキャッシュする。
/// 自分自身(TimeStop を持つオブジェクト)の Rigidbody は除外する。
/// </summary>
private void CacheAllRigidbodies()
{
rigidbodyStates.Clear();
rigidbody2DStates.Clear();
// 3D Rigidbody をすべて取得
Rigidbody[] allRigidbodies = includeInactiveObjects
? Resources.FindObjectsOfTypeAll<Rigidbody>()
: Object.FindObjectsByType<Rigidbody>(FindObjectsSortMode.None);
foreach (var rb in allRigidbodies)
{
if (rb == null) continue;
// 自分自身の Rigidbody は対象外
if (IsOnSameGameObjectOrChild(rb.gameObject, this.gameObject))
{
continue;
}
// まだ登録されていなければ追加
if (!rigidbodyStates.ContainsKey(rb))
{
RigidbodyState state = new RigidbodyState
{
velocity = rb.velocity,
angularVelocity = rb.angularVelocity,
constraints = rb.constraints,
isKinematic = rb.isKinematic
};
rigidbodyStates.Add(rb, state);
}
}
// 2D Rigidbody をすべて取得
Rigidbody2D[] allRigidbodies2D = includeInactiveObjects
? Resources.FindObjectsOfTypeAll<Rigidbody2D>()
: Object.FindObjectsByType<Rigidbody2D>(FindObjectsSortMode.None);
foreach (var rb2d in allRigidbodies2D)
{
if (rb2d == null) continue;
if (IsOnSameGameObjectOrChild(rb2d.gameObject, this.gameObject))
{
continue;
}
if (!rigidbody2DStates.ContainsKey(rb2d))
{
Rigidbody2DState state = new Rigidbody2DState
{
velocity = rb2d.velocity,
angularVelocity = rb2d.angularVelocity,
constraints = rb2d.constraints,
simulated = rb2d.simulated
};
rigidbody2DStates.Add(rb2d, state);
}
}
if (logDebugInfo)
{
Debug.Log($"[TimeStop] キャッシュ更新: Rigidbody = {rigidbodyStates.Count}, Rigidbody2D = {rigidbody2DStates.Count}");
}
}
/// <summary>
/// target が baseObj と同じ GameObject か、その子オブジェクトかどうか
/// </summary>
private bool IsOnSameGameObjectOrChild(GameObject target, GameObject baseObj)
{
if (target == baseObj) return true;
Transform t = target.transform;
while (t != null)
{
if (t == baseObj.transform) return true;
t = t.parent;
}
return false;
}
}
このコンポーネントは「世界の物理を止める」ことだけを担当し、
「いつ発動するか」「発動中にプレイヤーをどう動かすか」などは別コンポーネントに任せる設計になっています。
使い方の手順
ここでは、具体例として「プレイヤーだけが時間停止中も動ける」アクションゲームを想定して説明します。
手順①:TimeStopコンポーネントを配置する
- シーン内のプレイヤーの GameObject を選択します(例:
Player)。 Add ComponentボタンからTimeStopを追加します。- プレイヤーには
RigidbodyまたはRigidbody2Dが付いていても構いません。
TimeStop は「自分自身とその子オブジェクトの Rigidbody」は自動で除外するので、プレイヤーは止まりません。
手順②:InputAction を設定する(キーボードでトグル)
- Unity の Input System を使っている場合、Input Actions アセットを開きます。
- 任意の Action Map(例:
Player)にTimeStopToggleという Action を作成します。 - Action Type を
Buttonにし、Binding に Keyboard のTキーなどを設定します。 - 作成した Action を
TimeStopコンポーネントのToggle Actionスロットにドラッグ&ドロップします。 Use Input Actionにチェックが入っていることを確認します。
これで、ゲームプレイ中に T キーを押すと、世界の物理が停止 / 再開をトグルできます。
手順③:プレイヤーの移動コンポーネントはそのまま
プレイヤーの移動は、別のコンポーネント(例: PlayerMover)に分離しておきましょう。
たとえばこんな感じのシンプルな移動スクリプトでも動きます。
using UnityEngine;
using UnityEngine.InputSystem;
[RequireComponent(typeof(Rigidbody))]
public class PlayerMover : MonoBehaviour
{
[SerializeField] private float moveSpeed = 5f;
[SerializeField] private InputActionReference moveAction;
private Rigidbody rb;
private void Awake()
{
rb = GetComponent<Rigidbody>();
}
private void OnEnable()
{
if (moveAction != null)
{
moveAction.action.Enable();
}
}
private void OnDisable()
{
if (moveAction != null)
{
moveAction.action.Disable();
}
}
private void FixedUpdate()
{
Vector2 input = moveAction != null ? moveAction.action.ReadValue<Vector2>() : Vector2.zero;
Vector3 dir = new Vector3(input.x, 0f, input.y);
Vector3 targetVelocity = dir * moveSpeed;
// 物理ベースで移動
rb.velocity = new Vector3(targetVelocity.x, rb.velocity.y, targetVelocity.z);
}
}
TimeStop が世界の Rigidbody を止めている間も、プレイヤーの Rigidbody は対象外なので、
この PlayerMover は通常どおり動き続けます。
「時間が止まった世界でプレイヤーだけが動く」演出が簡単に作れますね。
手順④:敵や動く床など、物理で動くオブジェクトを配置する
- 敵キャラ
敵の GameObject にRigidbody+ 移動AIスクリプトを付けておくと、時間停止中はぴたりと停止します。
再開後は、停止前の速度・角速度が復元されるので、自然に動き出します。 - 動く床(Moving Platform)
物理ベースで動かしている床(Rigidbodyを使って移動させる)も、TimeStop の対象になります。
時間停止中は床の動きが止まり、再開すると元の速度で動き出します。 - 飛んでくる弾・投げられたオブジェクト
Rigidbodyを使って飛ばしている弾や投擲物も、空中でピタッと停止します。
再開すると、停止前の速度が復元され、軌道の続きをそのまま進みます。
このように、「物理で動いているもの」だけを一括で止めるので、
個別のスクリプトに「時間停止中なら動かさない」みたいな if 分岐を書かなくて済みます。
メリットと応用
TimeStop コンポーネントを導入すると、プレハブ管理やレベルデザインがかなり楽になります。
メリット①:プレハブを量産してもコードの変更がいらない
敵プレハブ、動く床プレハブ、ギミック用の物理オブジェクトなど、
どれも Rigidbody / Rigidbody2D を付けておくだけで、自動的に時間停止の対象になります。
- 新しい敵プレハブを追加しても、TimeStop のコードを一切触らなくてOK
- 「このオブジェクトだけ時間停止の影響を受けさせたくない」ときは、Rigidbody を使わずに Transform 直書きで動かすなど、設計で切り分けられる
TimeStop 自体は「物理を止めるだけ」に責務を絞っているので、
ゲームの仕様変更があっても影響範囲を把握しやすいのがポイントです。
メリット②:レベルデザイン時のテストがしやすい
レベルデザイナーがシーンに大量の物理オブジェクトを並べても、TimeStop をオンにするだけで挙動を一時停止できます。
「この瞬間の配置でスクリーンショットを撮りたい」「物理ギミックの途中状態をじっくり確認したい」といったときにも便利です。
メリット③:Godクラス化を防ぎ、責務が明確になる
時間停止機能を TimeStop コンポーネントに閉じ込めることで、
- プレイヤーの入力処理 → PlayerInput / PlayerMover コンポーネント
- 時間停止の制御 → TimeStop コンポーネント
- 演出(ポストエフェクト、UI表示など) → 別の TimeStopListener 的コンポーネント
というように、責務ごとにスクリプトを分割しやすくなります。
「時間停止したときに画面を暗くする」「SE を鳴らす」なども、TimeStop.IsTimeStopped を参照する小さなコンポーネントとして独立させると、さらに見通しが良くなります。
改造案:時間停止中にだけ発動するエフェクトを追加する
最後に、TimeStop を少し拡張したいときのサンプルとして、
「時間停止中だけパーティクルを出す」ような処理を追加する例を載せておきます。
TimeStop と同じ GameObject にアタッチして、TimeStop の状態を監視するコンポーネントとして分けるのがオススメです。
using UnityEngine;
/// <summary>
/// TimeStop が有効な間だけパーティクルを再生するコンポーネント。
/// TimeStop 本体とは責務を分けて、見た目の演出だけを担当する。
/// </summary>
[RequireComponent(typeof(TimeStop))]
public class TimeStopEffect : MonoBehaviour
{
[SerializeField] private ParticleSystem timeStopParticle;
private TimeStop timeStop;
private bool lastState;
private void Awake()
{
timeStop = GetComponent<TimeStop>();
}
private void Update()
{
if (timeStop == null || timeStopParticle == null) return;
// 状態が変化したタイミングだけ処理する
if (lastState != timeStop.IsTimeStopped)
{
lastState = timeStop.IsTimeStopped;
if (timeStop.IsTimeStopped)
{
// 時間停止開始時の演出
timeStopParticle.Play();
}
else
{
// 時間停止解除時の演出
timeStopParticle.Stop();
}
}
}
}
このように、「時間停止のロジック」と「演出」「入力」「プレイヤーの挙動」をそれぞれ小さなコンポーネントに分けていくと、
後から仕様を変えたり、別のシーン・別のゲームで再利用したりするときに、とても扱いやすくなります。
ぜひ TimeStop コンポーネントをベースに、自分のゲームならではの時間停止ギミックを育ててみてください。
