Godot 4 でアクションゲームや演出を作っていると、Engine.time_scale をいじってスローモーション演出を入れたくなること、多いですよね。
でも、素直に実装しようとすると:
- プレイヤーのスクリプトの中に「被弾時に
Engine.time_scale = 0.2」みたいなロジックを書き始める - 敵の必殺技でもスローを使いたくなって、また別のスクリプトに同じような処理を書く
- 結果として「どこで time_scale を変えてるのか」が散らばってカオスになる
…という「あるある」な状態になりがちです。
さらに、タイムスケールを戻し忘れてゲームがずっとスローのまま、なんてバグも起きやすいですね。
そこで今回は、「継承でプレイヤーや敵にスロー機能を埋め込む」のではなく、どのノードにも付け替え可能なコンポーネントとして、スローモーション演出を切り出してしまいましょう。
その名も SlowMotion コンポーネント。特定条件で Engine.time_scale を操作する、汎用トリガー部品として使えるようにしていきます。
【Godot 4】一瞬で“世界を遅くする”トリガー!「SlowMotion」コンポーネント
今回のコンポーネントのゴールはこんな感じです:
- 任意のノードにアタッチするだけで「スロー演出トリガー」になる
- シグナルで発火するので、プレイヤー・敵・ギミックなどから簡単に呼べる
- 自動で元の
time_scaleに戻す(戻し忘れ防止) - フェードイン / フェードアウトで、スローの入り方・戻り方を滑らかに制御
つまり、ゲーム中の「スロー演出」はすべてこのコンポーネントに任せてしまい、各キャラやギミックは「スローして」とシグナルを送るだけ、という構成にします。
フルコード:SlowMotion.gd
extends Node
class_name SlowMotion
## 特定条件で Engine.time_scale を操作するスローモーショントリガーコンポーネント。
##
## 使い方のイメージ:
## - 任意のシーンにこのコンポーネントを 1 つ置く(グローバルでもローカルでもOK)
## - プレイヤーや敵から `request_slow_motion()` をシグナルで呼ぶ
## - 一定時間だけスローになり、自動で元の time_scale に戻る
signal slow_started(target_scale: float)
signal slow_ended()
## --- 基本設定 ---
@export_range(0.01, 1.0, 0.01)
var slow_scale: float = 0.2:
set(value):
slow_scale = clamp(value, 0.01, 1.0)
## スロー継続時間(秒)
@export_range(0.0, 10.0, 0.1)
var slow_duration: float = 0.5
## スローに入るまでのフェード時間(秒)
@export_range(0.0, 5.0, 0.05)
var fade_in_time: float = 0.05
## 元の速度に戻るまでのフェード時間(秒)
@export_range(0.0, 5.0, 0.05)
var fade_out_time: float = 0.2
## 既にスロー中に再度リクエストが来たときの挙動
enum OverlapMode {
IGNORE, ## 無視する(今のスローが終わるまで何もしない)
RESTART, ## タイマーをリセットして、指定時間からやり直す
STACK_TIME ## 残り時間に slow_duration を加算(最大値は max_stack_duration)
}
@export var overlap_mode: OverlapMode = OverlapMode.RESTART
## STACK_TIME モード時の最大合計時間
@export_range(0.0, 30.0, 0.1)
var max_stack_duration: float = 3.0
## デバッグログを出すかどうか
@export var debug_log: bool = false
## --- 内部状態 ---
var _original_time_scale: float = 1.0
var _is_slow_active: bool = false
var _remaining_slow_time: float = 0.0
var _fade_in_timer: float = 0.0
var _fade_out_timer: float = 0.0
var _in_fade_in: bool = false
var _in_fade_out: bool = false
func _ready() -> void:
# 起動時の time_scale を保存しておく
_original_time_scale = Engine.time_scale
set_process(true)
if debug_log:
print("[SlowMotion] Ready. Original time_scale = ", _original_time_scale)
## 外部から呼ぶ用のメインAPI
## - 例: プレイヤーが被弾したときに呼ぶ
func request_slow_motion(
custom_slow_scale: float = -1.0,
custom_duration: float = -1.0
) -> void:
## 引数で値が指定されていなければ、export されたデフォルト値を使う
var target_scale := slow_scale if custom_slow_scale <= 0.0 else clamp(custom_slow_scale, 0.01, 1.0)
var duration := slow_duration if custom_duration < 0.0 else max(custom_duration, 0.0)
if duration == 0.0:
# duration 0 は無意味なので無視
if debug_log:
print("[SlowMotion] request ignored: duration == 0")
return
if _is_slow_active:
match overlap_mode:
OverlapMode.IGNORE:
if debug_log:
print("[SlowMotion] request ignored: already active (IGNORE)")
return
OverlapMode.RESTART:
if debug_log:
print("[SlowMotion] request: restart slow motion")
_start_slow_internal(target_scale, duration)
OverlapMode.STACK_TIME:
if debug_log:
print("[SlowMotion] request: stack time")
_remaining_slow_time = min(_remaining_slow_time + duration, max_stack_duration)
# すでにスロー中なので、scale だけ更新するかどうかは好み。
# ここでは「新しい target_scale を採用」する。
_set_time_scale_immediately(target_scale)
else:
if debug_log:
print("[SlowMotion] request: start new slow motion")
_start_slow_internal(target_scale, duration)
func _start_slow_internal(target_scale: float, duration: float) -> void:
_is_slow_active = true
_remaining_slow_time = duration
_in_fade_in = fade_in_time > 0.0
_in_fade_out = false
_fade_in_timer = 0.0
_fade_out_timer = 0.0
# フェードインが 0 の場合、即座に time_scale を変更
if not _in_fade_in:
_set_time_scale_immediately(target_scale)
else:
# フェードイン開始時は元のスケールから補間する
_set_time_scale_immediately(_original_time_scale)
emit_signal("slow_started", target_scale)
func _process(delta: float) -> void:
# time_scale 自体をいじっているので、ゲーム全体の delta が変化する点に注意。
# ここでは Engine.get_physics_ticks_per_second() などは使わず、単純に delta ベースで管理。
if not _is_slow_active:
return
# スロー中の経過時間を減らす
if _remaining_slow_time > 0.0 and not _in_fade_out:
_remaining_slow_time -= delta
if _remaining_slow_time <= 0.0:
# スロー継続時間が終わったのでフェードアウトへ
_in_fade_out = fade_out_time > 0.0
_in_fade_in = false
_fade_out_timer = 0.0
if not _in_fade_out:
# フェードアウト時間が 0 なら即座に元のスケールへ戻す
_end_slow_immediately()
# フェードイン処理
if _in_fade_in:
_fade_in_timer += delta
var t := clamp(_fade_in_timer / max(fade_in_time, 0.0001), 0.0, 1.0)
var target_scale := slow_scale
# 補間: 元のスケール → target_scale
var new_scale := lerp(_original_time_scale, target_scale, t)
Engine.time_scale = new_scale
if t >= 1.0:
_in_fade_in = false
Engine.time_scale = target_scale
# フェードアウト処理
if _in_fade_out:
_fade_out_timer += delta
var t2 := clamp(_fade_out_timer / max(fade_out_time, 0.0001), 0.0, 1.0)
var from_scale := Engine.time_scale
var new_scale2 := lerp(from_scale, _original_time_scale, t2)
Engine.time_scale = new_scale2
if t2 >= 1.0:
_end_slow_immediately()
func _set_time_scale_immediately(target_scale: float) -> void:
Engine.time_scale = target_scale
func _end_slow_immediately() -> void:
Engine.time_scale = _original_time_scale
_is_slow_active = false
_in_fade_in = false
_in_fade_out = false
_remaining_slow_time = 0.0
emit_signal("slow_ended")
if debug_log:
print("[SlowMotion] slow ended. time_scale restored to ", _original_time_scale)
## ゲーム終了時やシーン破棄時に time_scale を元に戻しておく安全策
func _exit_tree() -> void:
Engine.time_scale = _original_time_scale
if debug_log:
print("[SlowMotion] exit_tree: time_scale restored to ", _original_time_scale)
## 手動でスローを強制終了したいとき用のAPI
func cancel_slow() -> void:
if not _is_slow_active:
return
_end_slow_immediately()
使い方の手順
今回は、プレイヤーが被弾したときにスローになる例をベースに説明します。
ただし、敵の必殺技や動く罠の発動タイミングなどにも、そのまま使い回せます。
手順①:コンポーネントをプロジェクトに追加
res://components/SlowMotion.gdなど、好きな場所に上記コードを保存します。- Godot エディタで再読み込みすると、クラス名 SlowMotion がエディタから選べるようになります。
手順②:シーンに SlowMotion ノードを1つ置く
シーン構成の一例:
Main (Node) ├── Player (CharacterBody2D) │ ├── Sprite2D │ ├── CollisionShape2D │ └── PlayerController (Script) ├── EnemySpawner (Node) └── SlowMotion (SlowMotion) ← ここにコンポーネントを1つだけ置く
ポイント:
- SlowMotion は1シーンに1つでOK(ゲーム全体で共有するタイムスケールなので)。
- グローバルに使いたい場合は、
Autoloadシングルトンとして登録してもよいです。
手順③:プレイヤーから SlowMotion にアクセスして呼び出す
シンプルな例として、プレイヤーが敵にダメージを受けたときにスローを発動してみましょう。
PlayerController.gd(抜粋):
extends CharacterBody2D
@onready var slow_motion: SlowMotion = get_node("/root/Main/SlowMotion")
# ↑ シーン構成に合わせてパスは調整してください。
# Autoload にしているなら `get_node("/root/SlowMotion")` など。
func _on_hit_by_enemy() -> void:
# 被弾時にスローをリクエスト
# 引数なしなら、SlowMotion コンポーネント側の export 値を使用
slow_motion.request_slow_motion()
# もっと強い被弾時には、より長いスローを指定してもOK
# slow_motion.request_slow_motion(0.1, 1.0) # かなり強めのスローを1秒
このように、プレイヤー側には「スローの具体的な実装」を書かないのがポイントです。
「スローして」と依頼するだけで、演出の細かい調整はすべて SlowMotion コンポーネント側に閉じ込めます。
手順④:敵やギミックからも同じコンポーネントを使い回す
敵の必殺技でスローを使いたい場合も、同じコンポーネントを参照すればOKです。
extends Node2D
@onready var slow_motion: SlowMotion = get_node("/root/Main/SlowMotion")
func _on_super_attack_fired() -> void:
# 必殺技発動時に短くスロー
slow_motion.request_slow_motion(0.3, 0.3)
シーン構成イメージ:
Main (Node) ├── Player (CharacterBody2D) │ └── PlayerController (Script) ├── Boss (Node2D) │ └── BossAI (Script) └── SlowMotion (SlowMotion)
プレイヤーもボスも、同じ1つの SlowMotion コンポーネントを共有している状態です。
「スロー演出の中身」をどこか1か所で管理できるので、バランス調整がとても楽になりますね。
メリットと応用
この SlowMotion コンポーネントを導入することで、次のようなメリットがあります。
- シーン構造がスッキリ:
プレイヤーや敵のスクリプトにEngine.time_scaleの操作を埋め込まずに済むので、各スクリプトは「自分の責務」に集中できます。 - 演出の一元管理:
スローの強さ・長さ・フェード時間などを、SlowMotion コンポーネントの export 値だけで調整可能。
バランス調整時に「全シーンの全スクリプト」を探し回る必要がなくなります。 - 使い回しが容易:
別プロジェクトにもSlowMotion.gdをコピペして、シーンに1つ置くだけで同じ仕組みが使えます。 - 「継承の呪い」からの解放:
「スロー機能付きプレイヤー」「スロー機能付き敵」みたいなクラスを継承で増やす必要がありません。
どのノードでも、コンポーネントに向かってシグナルを送るだけでスローを発動できます。
さらに応用として、スロー開始・終了に合わせて画面エフェクトを入れるのも簡単です。
たとえば、SlowMotion コンポーネントに「画面を少し暗くする」処理を追加してみましょう。
改造案:スロー中だけ画面を暗くする
以下は、すでにシーン内に「画面暗転用の ColorRect」がある前提の改造案です。
@onready var dimmer: ColorRect = get_node_or_null("../ScreenDimmer")
func _on_slow_started(target_scale: float) -> void:
if dimmer:
dimmer.visible = true
dimmer.modulate.a = 0.5 # 半透明にする
func _on_slow_ended() -> void:
if dimmer:
dimmer.visible = false
実際には、slow_started / slow_ended シグナルを使って、別ノード側で画面エフェクトを制御する方が「責務の分離」としてはよりキレイです。
たとえば:
Main (Node) ├── SlowMotion (SlowMotion) ├── ScreenDimmer (ColorRect) └── SlowMotionEffects (Node) ← ここでシグナルを受けて画面エフェクトを制御
こんな感じで、「スロー演出の中心は SlowMotion コンポーネント」「見た目のエフェクトは別コンポーネント」と分けていくと、プロジェクトが大きくなっても破綻しにくくなりますね。
継承ではなく、コンポーネントを組み合わせてゲームを組み立てるスタイルで、どんどん Godot 4 ライフを快適にしていきましょう。
