Godotで「時間停止」っぽい表現をしようとすると、わりと面倒ですよね。
よくあるパターンとしては:
- プレイヤー、敵、ギミックそれぞれに「is_time_stopped」フラグを持たせる
- 各スクリプトの
_physics_process()の先頭でif is_time_stopped: returnと書きまくる - あるいは、共通のベースクラスを継承して、そこに時間停止ロジックを入れる
…みたいな実装をしがちです。
でもこれ、規模が大きくなるほど「全スクリプトに条件分岐を追加する地獄」になりますし、継承ツリーもどんどん肥大化していきます。
そこで今回は、「継承より合成」の思想に沿って、どのシーンにもポン付けできる時間停止コンポーネントを作ってみましょう。
その名も TimeStop コンポーネント。発動中は、自分以外の全てのノードの _physics_process を実質停止させます。
【Godot 4】世界を止めて自分だけ動く!「TimeStop」コンポーネント
今回のアプローチは:
- 「時間停止していないノード」には一切手を加えない(スクリプト修正不要)
- 「時間停止させたい側」にだけコンポーネントをアタッチする
- 物理フレーム毎に「世界の経過時間」を上書きすることで、他ノードの動きを止める
Godot 4 では SceneTree に physics_time_scale という便利なプロパティがあります。
これを 0 にすると「物理時間が進まない = _physics_process の delta が 0 になる」状態を作れます。
ただし、自分だけは動き続けたいので、TimeStop コンポーネントは:
- グローバルの
physics_time_scaleを 0 にして世界を止める - 自分の動きに使う「独自の時間」を内部で管理する
- 自分の制御スクリプトには、その「独自時間」を渡して動かす
つまり、「世界の時間」と「自分用の時間」を分離してしまう作戦ですね。
フルコード: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」アクションを追加
- メニューから Project > Project Settings… を開く
- Input Map タブで
time_stopというアクションを追加 - キーボードの
Qキーなど、好きなキーを割り当てる
手順②:TimeStop コンポーネントをシーンに追加
- Player シーンを開く
- Player の子として Node を追加し、スクリプトに
TimeStop.gdをアタッチ - 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()は 実時間ベースで動き続ける PlayerはTimeStopが持つ「独自時間差分」で動くので、プレイヤーだけ動く状態になる- 敵やギミックの
_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 コンポーネントを育てていきましょう。
