Godotで「時間停止」っぽい表現をしようとすると、わりと面倒ですよね。
よくあるパターンとしては:

  • プレイヤー、敵、ギミックそれぞれに「is_time_stopped」フラグを持たせる
  • 各スクリプトの _physics_process() の先頭で if is_time_stopped: return と書きまくる
  • あるいは、共通のベースクラスを継承して、そこに時間停止ロジックを入れる

…みたいな実装をしがちです。
でもこれ、規模が大きくなるほど「全スクリプトに条件分岐を追加する地獄」になりますし、継承ツリーもどんどん肥大化していきます。

そこで今回は、「継承より合成」の思想に沿って、どのシーンにもポン付けできる時間停止コンポーネントを作ってみましょう。
その名も TimeStop コンポーネント。発動中は、自分以外の全てのノードの _physics_process を実質停止させます。


【Godot 4】世界を止めて自分だけ動く!「TimeStop」コンポーネント

今回のアプローチは:

  • 「時間停止していないノード」には一切手を加えない(スクリプト修正不要)
  • 「時間停止させたい側」にだけコンポーネントをアタッチする
  • 物理フレーム毎に「世界の経過時間」を上書きすることで、他ノードの動きを止める

Godot 4 では SceneTreephysics_time_scale という便利なプロパティがあります。
これを 0 にすると「物理時間が進まない = _physics_processdelta が 0 になる」状態を作れます。

ただし、自分だけは動き続けたいので、TimeStop コンポーネントは:

  1. グローバルの physics_time_scale を 0 にして世界を止める
  2. 自分の動きに使う「独自の時間」を内部で管理する
  3. 自分の制御スクリプトには、その「独自時間」を渡して動かす

つまり、「世界の時間」と「自分用の時間」を分離してしまう作戦ですね。


フルコード:TimeStop.gd


extends Node
class_name TimeStop
## TimeStop コンポーネント
## - 発動中は SceneTree.physics_time_scale を 0 にして世界の物理時間を止める
## - 自分だけは「独自の時間」で動かし続ける想定
##
## 使い方の例は記事下部のチュートリアルを参照してください。

@export var toggle_action: StringName = "time_stop"
## 時間停止の ON/OFF を切り替える InputMap のアクション名
## 例: Project Settings > Input Map で "time_stop" を追加し、キーを割り当てる

@export var max_duration: float = 3.0
## 連続して時間停止できる最大秒数(0以下なら無制限)

@export var cooldown: float = 2.0
## 時間停止解除後に、再度発動できるまでのクールダウン秒数

@export var affect_physics_only: bool = true
## true の場合: physics_time_scale のみ 0 にして物理処理だけを止める
## false の場合: time_scale も 0 にして、_process() 系も止める(アニメも止まる)

@export var auto_disable_on_tree_exit: bool = true
## このノードがツリーから抜けるときに、必ず時間停止を解除するかどうか

signal time_stop_started
## 時間停止が開始されたときに発火

signal time_stop_ended
## 時間停止が終了したときに発火

signal time_stop_cooldown_started(remaining: float)
## クールダウンが開始されたときに発火

signal time_stop_cooldown_ended
## クールダウンが終了したときに発火


var _is_time_stopped: bool = false
var _cooldown_remaining: float = 0.0
var _duration_elapsed: float = 0.0

var _original_physics_time_scale: float = 1.0
var _original_time_scale: float = 1.0

## 自分専用の「独立した時間」を進めるためのタイマー
var _local_time: float = 0.0
var _last_real_time: float = 0.0


func _ready() -> void:
    # 現在のタイムスケールを保存しておく
    _original_physics_time_scale = get_tree().physics_time_scale
    _original_time_scale = get_tree().time_scale
    _last_real_time = Time.get_ticks_msec() / 1000.0


func _process(delta: float) -> void:
    # クールダウン中なら残り時間を減らす
    if _cooldown_remaining > 0.0:
        _cooldown_remaining -= delta
        if _cooldown_remaining <= 0.0:
            _cooldown_remaining = 0.0
            time_stop_cooldown_ended.emit()

    # 入力で時間停止のトグル
    if Input.is_action_just_pressed(toggle_action):
        if not _is_time_stopped:
            _try_start_time_stop()
        else:
            _end_time_stop()

    # 時間停止中の経過時間管理
    if _is_time_stopped:
        if max_duration > 0.0:
            _duration_elapsed += delta
            if _duration_elapsed >= max_duration:
                _end_time_stop()

    # 「世界の時間」とは別に、自分専用の時間を進める
    var now := Time.get_ticks_msec() / 1000.0
    var real_delta := now - _last_real_time
    _last_real_time = now
    _local_time += real_delta


func _exit_tree() -> void:
    # ツリーから抜けるときは、念のため時間停止を解除しておく
    if auto_disable_on_tree_exit and _is_time_stopped:
        _restore_time_scale()


func _try_start_time_stop() -> void:
    if _cooldown_remaining > 0.0:
        return  # クールダウン中なので発動不可

    _start_time_stop()


func _start_time_stop() -> void:
    if _is_time_stopped:
        return

    var tree := get_tree()

    # 現在のスケールを保存
    _original_physics_time_scale = tree.physics_time_scale
    _original_time_scale = tree.time_scale

    # 物理時間を止める(必要なら通常の time_scale も止める)
    tree.physics_time_scale = 0.0
    if not affect_physics_only:
        tree.time_scale = 0.0

    _is_time_stopped = true
    _duration_elapsed = 0.0
    time_stop_started.emit()


func _end_time_stop() -> void:
    if not _is_time_stopped:
        return

    _restore_time_scale()
    _is_time_stopped = false
    time_stop_ended.emit()

    # クールダウン開始
    if cooldown > 0.0:
        _cooldown_remaining = cooldown
        time_stop_cooldown_started.emit(_cooldown_remaining)


func _restore_time_scale() -> void:
    var tree := get_tree()
    tree.physics_time_scale = _original_physics_time_scale
    if not affect_physics_only:
        tree.time_scale = _original_time_scale


## 公開API群

func is_time_stopped() -> bool:
    ## 現在、世界が時間停止中かどうか
    return _is_time_stopped


func get_cooldown_remaining() -> float:
    ## 残りクールダウン秒数(0ならすぐ使える)
    return _cooldown_remaining


func get_local_time() -> float:
    ## このコンポーネントが持つ「独自の時間」(秒)
    ## time_stop の ON/OFF に関係なく、常に実時間ベースで進む
    return _local_time


func get_local_delta() -> float:
    ## 直近フレームの「独自 delta 秒」
    ## - 通常の delta は time_scale の影響を受けるが、
    ##   こちらは Time.get_ticks_msec() ベースで計算する想定。
    ##
    ## 注意: この関数を呼ぶ側で、前フレームの local_time を記録して
    ## 差分を取る方が正確なので、あくまで参考実装。
    return get_process_delta_time()

使い方の手順

ここからは、具体的なプレイヤー例を通して使い方を見ていきましょう。

例1:プレイヤーだけが動ける時間停止

想定シーン構成:

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 ├── TimeStop (Node)  ← 今回のコンポーネント
 └── PlayerController (Node or Script)

手順①:InputMap に「time_stop」アクションを追加

  1. メニューから Project > Project Settings… を開く
  2. Input Map タブで time_stop というアクションを追加
  3. キーボードの Q キーなど、好きなキーを割り当てる

手順②:TimeStop コンポーネントをシーンに追加

  1. Player シーンを開く
  2. Player の子として Node を追加し、スクリプトに TimeStop.gd をアタッチ
  3. Inspector で以下を設定
    • toggle_action : "time_stop"
    • max_duration : 3.0(3秒間停止)
    • cooldown : 2.0(2秒クールダウン)
    • affect_physics_only : true(物理だけ止める)

手順③:プレイヤーの移動スクリプトを「独自時間」で動かす

ポイントは、「世界の delta」ではなく TimeStop のローカル時間差分で動かすことです。
次のようなコントローラを Player に付けてみましょう:


extends CharacterBody2D

@export var move_speed: float = 200.0

var _time_stop: TimeStop
var _last_local_time: float = 0.0


func _ready() -> void:
    # 同じシーン内の TimeStop コンポーネントを取得
    _time_stop = get_node_or_null("TimeStop")
    if _time_stop:
        _last_local_time = _time_stop.get_local_time()


func _physics_process(delta: float) -> void:
    var real_delta := delta

    # TimeStop が存在するなら、その「独自時間」の差分を使う
    if _time_stop:
        var current_local_time := _time_stop.get_local_time()
        real_delta = current_local_time - _last_local_time
        _last_local_time = current_local_time

    # ここから下は、いつも通りの移動ロジックでOK
    var input_dir := Vector2.ZERO
    input_dir.x = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
    input_dir.y = Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")

    if input_dir.length() > 1.0:
        input_dir = input_dir.normalized()

    velocity = input_dir * move_speed
    move_and_slide()

このようにすると:

  • 時間停止して physics_time_scale = 0 になっても、TimeStop_process()実時間ベースで動き続ける
  • PlayerTimeStop が持つ「独自時間差分」で動くので、プレイヤーだけ動く状態になる
  • 敵やギミックの _physics_process()delta = 0 になり、実質停止する

手順④:敵や動く床は「いつも通り」書くだけ

例えば、敵や動く床はこういうスクリプトでOKです:


# Enemy.gd
extends CharacterBody2D

@export var move_speed: float = 100.0

func _physics_process(delta: float) -> void:
    # ここでは delta をそのまま使う
    # TimeStop が発動すると physics_time_scale = 0 になるので、
    # delta も 0 になり、勝手に止まってくれる
    velocity.x = move_speed
    move_and_slide()

敵シーンの構成例:

Enemy (CharacterBody2D)
 ├── Sprite2D
 └── CollisionShape2D

TimeStop を付ける必要はありません。
コンポーネントは「時間を止める側(プレイヤーなど)」だけが持っていればOKです。


メリットと応用

この TimeStop コンポーネントを使うことで、次のようなメリットがあります。

  • 「止まる側」には一切手を入れなくてよい
    敵やギミックは、今まで通り _physics_process(delta) を書くだけで、TimeStop の存在を意識しなくて済みます。
  • プレイヤー側も「TimeStop 用の if 文地獄」から解放
    「時間停止中ならこの処理はスキップ」みたいな分岐を書きまくらなくてOK。
    ただ real_delta を TimeStop 経由で決めるだけです。
  • シーン構造がシンプルなまま
    巨大な共通ベースクラスを作らず、TimeStop という1コンポーネントに責務を押し込めるので、ツリーもスクリプトもスッキリします。
  • 他のシーンにも簡単に再利用可能
    「ボスだけ時間を止められる」「特定のギミックだけ時間停止」など、TimeStop を別のノードに付け替えるだけで応用が効きます。

さらに、このコンポーネントはシグナルを持っているので、UI やエフェクトとも連携しやすいです。

  • time_stop_started : 画面を青くフィルタリング、SE 再生、ポーズエフェクト表示など
  • time_stop_ended : エフェクト解除
  • time_stop_cooldown_started / _ended : クールダウンゲージの表示

改造案:時間停止中だけ色を変える

最後に、簡単な改造案として「時間停止中はプレイヤーの色を変える」処理を追加してみましょう。

Player 側にこんな関数を用意して、TimeStop のシグナルに接続するだけです:


func connect_time_stop_signals(time_stop: TimeStop) -> void:
    # エディタからでも、コードからでも接続OK
    time_stop.time_stop_started.connect(func ():
        var sprite := get_node_or_null("Sprite2D")
        if sprite:
            sprite.modulate = Color(0.5, 0.8, 1.0)  # 青っぽく
    )

    time_stop.time_stop_ended.connect(func ():
        var sprite := get_node_or_null("Sprite2D")
        if sprite:
            sprite.modulate = Color.WHITE
    )

このように、「時間を止める」というゲーム的に重たい機能も、1つのコンポーネントに閉じ込めて合成で使い回すと、かなり管理しやすくなります。
ぜひ自分のプロジェクト用にカスタマイズして、TimeStop コンポーネントを育てていきましょう。