Godot 4でアクションゲームを作っていると、プレイヤーや敵キャラに「スタン状態」をつけたくなること、ありますよね。
でも、素直に実装しようとすると…
- プレイヤー用のベースクラスに
is_stunnedフラグを足す _physics_process()のあちこちに「スタン中なら return」みたいな条件分岐を追加- 敵クラスにも同じような処理をコピペ
…と、どんどん「継承ツリー+条件分岐地獄」になりがちです。
さらに「スタン中は頭の上にピヨピヨマークを出したい」とか「スタン解除時にSEを鳴らしたい」などの仕様が増えると、ベースクラスが太りすぎて管理がつらくなります。
そこで今回は、「スタン状態」を 1 つのコンポーネントとして切り出してしまいましょう。
どんなキャラにもポン付けできる 「StunEffect」コンポーネントを作って、親ノードの _physics_process() を一時停止しつつ、頭上にピヨりマークを表示できるようにします。
【Godot 4】一瞬でスタン演出を合成!「StunEffect」コンポーネント
このコンポーネントのゴールはシンプルです。
- 親ノード(プレイヤーや敵など)の
_physics_process()を「スタン中だけ実行させない」 - スタン中は頭上にピヨりマーク(任意のシーン)を表示する
- タイマーで自動解除もできるし、スクリプトから手動解除もできる
- どのノードにも「アタッチするだけ」で導入できる(継承不要)
では、フルコードから見ていきましょう。
StunEffect.gd フルコード
extends Node
class_name StunEffect
## 親ノードの「スタン状態」を管理するコンポーネント。
## - スタン中は親の _physics_process() を停止
## - 頭上にピヨりマークを表示
## - タイマーで自動解除 or 手動解除の両対応
@export var auto_recover: bool = true:
set(value):
auto_recover = value
if not auto_recover and is_instance_valid(_timer):
_timer.stop()
## スタンを自動解除するかどうか。
## true なら duration 秒後に自動的に解除します。
@export_range(0.0, 60.0, 0.1, "suffix:s") var duration: float = 1.5
## スタン継続時間(秒)。auto_recover が true のときのみ使用されます。
@export var stun_marker_scene: PackedScene
## ピヨりマークのシーン。
## 例: PiyoMarker.tscn(AnimatedSprite2D や Sprite2D など)
## 未設定の場合はマーカー表示をスキップします。
@export var marker_offset: Vector2 = Vector2(0, -32)
## ピヨりマークの表示位置オフセット。
## 親ノードのローカル座標からの相対位置です。
@export var disable_parent_physics_process: bool = true
## true の場合、スタン中は親ノードの _physics_process() を止めます。
## 物理挙動を完全に止めたいときは true 推奨。
@export var disable_parent_input: bool = true
## true の場合、親が _unhandled_input() を持っていれば、
## スタン中は入力処理をスキップするためのフラグを立てます。
## 実際のスキップ処理は、親スクリプト側で is_stunned フラグを見る形で実装します。
var _is_stunned: bool = false setget _set_is_stunned
## 内部状態としてのスタンフラグ。
## 親からは is_stunned プロパティ経由で参照可能です。
var is_stunned: bool:
get:
return _is_stunned
var _parent_original_physics_process: bool = true
## 親ノードが元々 _physics_process() を有効にしていたかどうかを保存します。
var _marker_instance: Node2D
var _timer: Timer
func _ready() -> void:
# 親ノードを取得しておく(存在しない場合は警告を出す)
if get_parent() == null:
push_warning("StunEffect: 親ノードが存在しません。このコンポーネントは何かの子ノードとして使ってください。")
# タイマーを生成して子として追加
_timer = Timer.new()
_timer.one_shot = true
_timer.autostart = false
add_child(_timer)
_timer.timeout.connect(_on_timer_timeout)
# 親の元々の physics_process 有効状態を記録
if get_parent() != null:
_parent_original_physics_process = get_parent().is_physics_processing()
# すでに is_stunned が true なら(シーン上で初期値を変えた場合など)即スタン開始
if _is_stunned:
_apply_stun_state()
func _physics_process(delta: float) -> void:
# ピヨりマークを親の位置に追従させる
if _is_stunned and is_instance_valid(_marker_instance) and is_instance_valid(get_parent()):
var parent_2d := get_parent() as Node2D
if parent_2d:
_marker_instance.global_position = parent_2d.global_position + marker_offset
func stun(duration_override: float = -1.0) -> void:
## 外部からスタンを開始するためのメソッド。
## duration_override >= 0 の場合、その値でスタン時間を上書きします。
if _is_stunned:
# すでにスタン中なら、タイマーだけ延長(auto_recover が有効な場合)
if auto_recover and is_instance_valid(_timer):
var d := duration_override if duration_override >= 0.0 else duration
_timer.start(d)
return
_is_stunned = true
_apply_stun_state()
if auto_recover:
var d := duration_override if duration_override >= 0.0 else duration
_timer.start(d)
func recover() -> void:
## 外部からスタン解除するためのメソッド。
if not _is_stunned:
return
_is_stunned = false
_clear_stun_state()
# タイマーは止めておく
if is_instance_valid(_timer):
_timer.stop()
func toggle() -> void:
## デバッグ用:スタン状態をトグルします。
if _is_stunned:
recover()
else:
stun()
func _set_is_stunned(value: bool) -> void:
# エディタから is_stunned をいじられた場合に対応するためのセッター
if _is_stunned == value:
return
_is_stunned = value
if not is_inside_tree():
return
if _is_stunned:
_apply_stun_state()
else:
_clear_stun_state()
func _apply_stun_state() -> void:
## スタン開始時の処理
var parent := get_parent()
if parent == null:
return
# 親の _physics_process を止める
if disable_parent_physics_process:
_parent_original_physics_process = parent.is_physics_processing()
parent.set_physics_process(false)
# 親に is_stunned プロパティがあれば、それも true にしてあげる
if "is_stunned" in parent:
parent.is_stunned = true
# 親が「入力を止めるフラグ」を持っている想定の場合
if disable_parent_input and "can_receive_input" in parent:
parent.can_receive_input = false
# ピヨりマークを生成
_spawn_marker()
func _clear_stun_state() -> void:
## スタン解除時の処理
var parent := get_parent()
if parent == null:
return
# 親の _physics_process を元の状態に戻す
if disable_parent_physics_process:
parent.set_physics_process(_parent_original_physics_process)
# 親に is_stunned プロパティがあれば false に
if "is_stunned" in parent:
parent.is_stunned = false
# 入力フラグも戻す
if disable_parent_input and "can_receive_input" in parent:
parent.can_receive_input = true
# ピヨりマークを削除
_despawn_marker()
func _spawn_marker() -> void:
## ピヨりマークの生成
_despawn_marker() # 二重生成防止
if stun_marker_scene == null:
# シーン未指定なら何もしない
return
var instance := stun_marker_scene.instantiate()
# 2D 用のマーカーを想定
if not instance is Node2D:
push_warning("StunEffect: stun_marker_scene は Node2D 系のシーンを推奨します。")
_marker_instance = instance
get_tree().current_scene.add_child(_marker_instance)
# 初期位置を設定
var parent_2d := get_parent() as Node2D
if parent_2d:
_marker_instance.global_position = parent_2d.global_position + marker_offset
func _despawn_marker() -> void:
## ピヨりマークの削除
if is_instance_valid(_marker_instance):
_marker_instance.queue_free()
_marker_instance = null
func _on_timer_timeout() -> void:
## タイマー満了時(自動解除)
if _is_stunned:
recover()
使い方の手順
ここからは、実際にプレイヤーや敵に組み込む手順を見ていきましょう。
例1: プレイヤーにスタンを付与する
想定するシーン構成はこんな感じです。
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── StunEffect (Node)
手順①: ピヨりマーク用シーンを用意する
- 新規シーンを作成し、ルートを
Node2DまたはSprite2Dにします。 - 名前を
PiyoMarkerなどにし、画像やアニメーションを設定します。 - このシーンを
res://effects/PiyoMarker.tscnなどのパスで保存します。
シンプルな例:
PiyoMarker (Node2D) └── Sprite2D (ピヨピヨ画像)
手順②: プレイヤーシーンに StunEffect を追加
- プレイヤーシーンを開きます(
Player.tscn)。 - ルートの
CharacterBody2Dを右クリック → 「子ノードを追加」。 Nodeを追加し、名前をStunEffectに変更。StunEffectノードに、先ほどのStunEffect.gdスクリプトをアタッチします。- インスペクタで以下を設定します:
StunEffect.stun_marker_sceneにPiyoMarker.tscnを指定duration(スタン時間)を好みの秒数にmarker_offsetで頭上の位置を微調整
手順③: プレイヤースクリプトからスタンを呼び出す
プレイヤー側のスクリプト(例: Player.gd)は、できるだけシンプルに保ちたいので、
スタンロジックは StunEffect に丸投げします。
extends CharacterBody2D
@export var move_speed: float = 200.0
var is_stunned: bool = false
var can_receive_input: bool = true
@onready var stun_effect: StunEffect = $StunEffect
func _physics_process(delta: float) -> void:
# StunEffect が disable_parent_physics_process = true の場合、
# スタン中はそもそもこの関数が呼ばれないので、ここでの分岐は必須ではありません。
# ただし将来の拡張のために、あえてフラグを残しています。
if is_stunned:
velocity = Vector2.ZERO
move_and_slide()
return
# 通常の移動処理
var input_vector = Vector2.ZERO
if can_receive_input:
input_vector.x = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
input_vector.y = Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")
if input_vector.length() > 0.0:
input_vector = input_vector.normalized()
velocity = input_vector * move_speed
move_and_slide()
func take_damage(amount: int, cause_stun: bool = false) -> void:
# 何らかのダメージ処理
print("Player took ", amount, " damage.")
if cause_stun:
# スタンを付与(1.5秒だけスタンさせる例)
stun_effect.stun(1.5)
これで、「ダメージを受けたときだけスタンさせる」「特定の攻撃だけスタン付与」などが簡単に制御できます。
手順④: 敵キャラにもコピペで導入
敵キャラにも同じコンポーネントを使いまわしてみましょう。
Enemy (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── StunEffect (Node)
敵スクリプト側は、プレイヤーと同じように StunEffect を参照するだけです。
extends CharacterBody2D
var is_stunned: bool = false
var can_receive_input: bool = true
@onready var stun_effect: StunEffect = $StunEffect
func _physics_process(delta: float) -> void:
if is_stunned:
velocity = Vector2.ZERO
move_and_slide()
return
# ここにパトロール AI や追尾処理などを書く
# 例: プレイヤーに向かって移動するなど
func stun_from_player_attack() -> void:
# プレイヤー攻撃から呼ばれる想定
stun_effect.stun(2.0) # 敵は 2 秒スタンさせる例
プレイヤーと敵で全く同じコンポーネントを使い回せているのがポイントですね。
メリットと応用
この StunEffect コンポーネントを導入すると、いくつか嬉しいポイントがあります。
- 継承ツリーをいじらなくていい
既存のPlayer.gdやEnemy.gdをほぼそのままに、スタン機能だけ後付けできます。 - 「スタン」の仕様変更が一箇所で完結
スタン時間、ピヨりマークの見た目、親の_physics_process()の止め方などを、
コンポーネント内のコードだけ触れば全キャラに反映されます。 - シーン構造がスッキリする
「スタン用の子ノード」「タイマー」「エフェクト」などを個別に生やす必要がなく、StunEffectひとつにまとまっているので、シーンツリーが見やすくなります。 - レベルデザイン時に「スタンの有無」をノード単位で切り替えられる
「この敵だけスタン無効にしたい」という場合は、単にStunEffectノードを削除するだけでOKです。
応用としては、例えば以下のような拡張が考えられます。
- スタン開始・解除時にサウンドエフェクトを鳴らす
- スタン中は色を変える・シェーダーで点滅させる
- スタン中は特定の入力だけ許可(ジャンプだけOKなど)
最後に、スタン開始時に親ノードの色を少し暗くする簡単な改造案を載せておきます。
func _apply_stun_state() -> void:
var parent := get_parent()
if parent == null:
return
if disable_parent_physics_process:
_parent_original_physics_process = parent.is_physics_processing()
parent.set_physics_process(false)
if "is_stunned" in parent:
parent.is_stunned = true
if disable_parent_input and "can_receive_input" in parent:
parent.can_receive_input = false
# --- ここから追加: 親の Modulate を暗くする ---
var parent_2d := parent as CanvasItem
if parent_2d:
parent_2d.modulate = Color(0.7, 0.7, 0.7)
# --- ここまで追加 ---
_spawn_marker()
func _clear_stun_state() -> void:
var parent := get_parent()
if parent == null:
return
if disable_parent_physics_process:
parent.set_physics_process(_parent_original_physics_process)
if "is_stunned" in parent:
parent.is_stunned = false
if disable_parent_input and "can_receive_input" in parent:
parent.can_receive_input = true
# --- ここから追加: 親の Modulate を元に戻す ---
var parent_2d := parent as CanvasItem
if parent_2d:
parent_2d.modulate = Color.WHITE
# --- ここまで追加 ---
_despawn_marker()
このように、「スタン時にやりたいこと」をどんどん StunEffect に合成していけば、
プレイヤーや敵のスクリプトはどんどんシンプルになっていきます。
継承より合成、どんどん進めていきましょう。
