プレイヤーがダメージを受けたとき、HPバーを減らし、効果音を鳴らし、画面を揺らし、ゲームオーバー判定もしたい。Godotを始めたばかりだと、つい get_node() で全部の相手を探して直接呼びたくなります。
小さな試作ならそれでも動きます。ただ、UIの階層を変えた瞬間に壊れたり、プレイヤーを別シーンで使い回せなくなったりします。この記事では、Godotの シグナル を使って「イベントが起き たことだけを知らせる」設計に変える方法を解説します。
この記事でわかること
signal、connect()、emit()の役割- UI、サウンド、ゲーム進行をプレイヤーから切り離す考え方
- エディタ接続とコード接続の使い分け
- 直接呼び出しとシグナルの判断基準
シグナルは「できごとの通知」
シグナルは、ある ノードが「何かが起きた」と外へ知らせるための仕組みです。発信する側は、誰が聞いているかを知りません。聞きたい側だけが、そのシグナルに接続しておきます。

たとえば Player は「HPが変わった」とだけ通知します。HUD はHPバーを更新し、Sound はダメージ音を鳴らし、GameManager は死亡判定をします。Player はそれらのノード名も、シーンツリー上の場所も知る必要がありません。
この「通知する側」と「受け取る側」を切り離した状態を 疎結合 と呼びます。言い換えると、片方を変更しても、もう片方へ影響が広がりにくい設計です。
悪い例から見ると、メリットが分かりやすいです。
# Player.gd
# 悪い例: PlayerがUI、音、画面遷移の場所を全部知っている
func take_damage(amount: int) -> void:
current_health = max(0, current_health - amount)
get_node("/root/Main/HUD/HealthBar").value = current_health
get_node("/root/Main/SoundManager").play_damage()
if current_health <= 0:
get_node("/root/Main/GameManager").show_game_over()
このコードは、ノード階層が少し変わるだけで壊れます。さらに Player をボス戦、チュートリアル、ミニゲームで再利用しようとしても、毎回同じ HUD や SoundManager が必要になります。
ゲーム開発ではどこで使う?
シグナルは「1つのできごとを、複数の場所がそれぞれの責任で処理する」場面に向いています。

- RPG: 宝箱を開けたら
quest_doneを通知し、クエストログ、報酬UI、実績システムが反応する。 - タワーディフェンス: 敵がゴールへ到達したら
goal_reachedを通知し、ライフ表示、効果音、ウェーブ管理が反応する。 - パズル: コンボが成立したら
comboを通知し、スコア、演出、制限時間ボーナスが反応する。
ポイントは、宝箱や敵やパズル盤が「通知先の全部」を知らなくてよいことです。後から実績システムを足しても、発信側のコードを増やさず、受信側を接続するだけで済みます。
シグナルの基本形
シグナルは、次の3つで考えると整理しやすいです。
| 要素 | 役割 | 例 |
|---|---|---|
signal | 通知の名前を定義する | signal health_changed(current, max) |
connect() | 通知を受け取る関数を登録する | player.health_changed.connect(...) |
emit() | 通知を発信する | health_changed.emit(current_health, max_health) |
最小の例はこうです。
# Player.gd
extends CharacterBody2D
class_name Player
signal health_changed(current_health: int, max_health: int)
@export var max_health: int = 100
var current_health: int = max_health
func take_damage(amount: int) -> void:
current_health = max(0, current_health - amount)
# HPが変わった事実だけを外へ通知する
health_changed.emit(current_health, max_health)
signal health_changed(...) は「このノードはHP変更通知を出せます」という宣言です。emit() は実際に通知を発信する処理です。
受け取る側は、接続された関数の引数をシグナル側とそろえます。
# Hud.gd
extends CanvasLayer
@onready var health_bar: TextureProgressBar = $HealthBar
func _on_player_health_changed(current_health: int, max_health: int) -> void:
health_bar.max_value = max_health
health_bar.value = current_health
実践:ダメージ通知をシグナル化する
ここでは、アクションゲームのプレイヤーを題材にします。ダメージを受けたら、HPバー、効果音、ゲームオーバー処理がそれぞれ反応する構成です。
まず発信側の Player は、自分の体力だけを管理します。
# Player.gd
extends CharacterBody2D
class_name Player
signal health_changed(current_health: int, max_health: int)
signal died
@export var max_health: int = 100
var current_health: int
func _ready() -> void:
current_health = max_health
health_changed.emit(current_health, max_health)
func take_damage(amount: int) -> void:
if current_health <= 0:
return
current_health = max(0, current_health - amount)
# UIや効果音のことは知らず、HP変更だけを通知する
health_changed.emit(current_health, max_health)
if current_health == 0:
# 死亡時の反応は受信側に任せる
died.emit()
Player は HUD も Sound も GameManager も直接呼びません。「HPが変わった」「死亡した」という事実だけを発信します。
受信側の HUD は、HPバーの表示だけを担当します。
# Hud.gd
extends CanvasLayer
class_name Hud
@onready var health_bar: TextureProgressBar = $HealthBar
func setup(player: Player) -> void:
# HUDはHP変更通知を受け取り、自分の表示だけを更新する
player.health_changed.connect(_on_player_health_changed)
func _on_player_health_changed(current_health: int, max_health: int) -> void:
health_bar.max_value = max_health
health_bar.value = current_health
ゲーム進行を担当する GameManager は、死亡時のシーン遷移だけを担当します。
# GameManager.gd
extends Node
@export var player: Player
@export var hud: Hud
func _ready() -> void:
hud.setup(player)
# ゲーム進行側は死亡通知だけを受け取る
player.died.connect(_on_player_died)
func _on_player_died() -> void:
get_tree().change_scene_to_file("res://scenes/game_over.tscn")
この構成なら、後から DamageFlash や AchievementTracker を足したくなっても、Player.gd は変更せずに済みます。受信側を1つ増やして player.died.connect(...) するだけです。
エディタ接続とコード接続
Godotでは、シグナルをエディタから接続することも、GDScriptから接続することもできます。
エディタ接続が向いている場面
Button.pressed のように、シーン内で固定されたUIをつなぐ場合はエディタ接続が分かりやすいです。
- シーンツリーで
Buttonを選ぶ。 - 右側の「ノード」タブから
pressed()を選ぶ。 - 接続先ノードと関数名を選ぶ。
# TitleMenu.gd
func _on_start_button_pressed() -> void:
get_tree().change_scene_to_file("res://scenes/stage_01.tscn")
UIボタン、チェックボックス、メニュー項目のように「このシーン内で関係が固定されているもの」は、エディタ接続でも十分です。
コード接続が向いている場面
敵、弾、アイテム、ポップアップUIのように、実行中に生成されるノードはコード接続が向いています。
# WaveSpawner.gd
extends Node
const ENEMY_SCENE = preload("res://scenes/enemy.tscn")
signal wave_enemy_defeated(score: int)
func spawn_enemy() -> void:
var enemy = ENEMY_SCENE.instantiate()
add_child(enemy)
# 実行中に作った敵は、生成したタイミングで接続する
enemy.defeated.connect(_on_enemy_defeated)
func _on_enemy_defeated(score: int) -> void:
# WaveSpawnerの外へ、敵撃破をさらに通知する
wave_enemy_defeated.emit(score)
「いつ何個生成されるか分からないもの」は、生成したタイミングで接続するのが自然です。
直接呼び出しとの使い分け
シグナルは便利ですが、何でもシグナルにすると逆に読みづらくなります。判断基準はシンプルです。

「相手を知っているべき処理か?」 と自問してください。
- 直接呼ぶ: 親が子のドアを開ける、インベントリUIが自分のスロットを更新する、武器が自分の弾を生成する。
- シグナルにする: プレイヤーが死亡した、スコアが増えた、敵がゴールした、クエストが完了した。
直接呼び出しは「命令」です。シグナルは「通知」です。
DoorController が特定の Door を開けるなら、door.open() の方が読みやすいです。逆に Player が死亡したとき、UI、音、カメラ、シーン遷移、実績の全部を直接知り始めたら、シグナルへ切り替える合図です。
よくあるつまずき
接続したのに呼ばれない
まず、emit() が実行されているかを確認します。
func take_damage(amount: int) -> void:
current_health = max(0, current_health - amount)
# まずemitされているかをログで確認する
print_debug("emit health_changed: ", current_health)
health_changed.emit(current_health, max_health)
emit() が出ているのに受信側が呼ばれない場合は、接続先のノード参照、関数名、引数の数を確認します。
二重に呼ばれる
_ready() が再実行されるたびに同じシグナルを接続していると、1回の emit() で同じ関数が複数回呼ばれる ことがあります。動的に接続する場合は、必要に応じて接続済みかを確認します。
func bind_player(player: Player) -> void:
# すでに接続済みなら、同じ関数を二重登録しない
if not player.health_changed.is_connected(_on_player_health_changed):
player.health_changed.connect(_on_player_health_changed)
長生きするノードへの接続を外し忘れる
通常のシーン内だけで完結する接続は、ノード破棄とともに問題なく扱えることが多いです。一方、Autoloadのようにゲーム中ずっと残るノードへ接続する場合は、短命なUI側が消えるタイミングで disconnect() を検討します。
# PauseMenu.gd
func _ready() -> void:
GameEvents.pause_changed.connect(_on_pause_changed)
func _exit_tree() -> void:
# 長生きするAutoloadへの接続は、UIが消える時に外しておく
if GameEvents.pause_changed.is_connected(_on_pause_changed):
GameEvents.pause_changed.disconnect(_on_pause_changed)
おまけ:シグナルバスを使う場面
シグナルに慣れると、「どこからでもイベントを投げたい」と感じる場面が出てきます。そのとき候補になるのが、Autoloadにシグナルだけを集める シグナルバス です。
# GameEvents.gd(Autoloadに登録)
extends Node
signal coins_changed(total: int)
signal achievement_unlocked(achievement_id: String)
signal pause_changed(is_paused: bool)
これは便利ですが、最初から全部をシグナルバスに流す必要はありません。近いノード同士の通知は普通のシグナルで十分です。シーンをまたいだ通知、複数の独立システムが聞く通知だけを、シグナルバスへ寄せると管理しやすくなります。
Autoload自体の考え方は、 Autoloadによるシーンをまたぐデータ管理 で詳しく扱います。
まとめ
シグナルは、Godotでノード同士をきれいに連携させるための基本機能です。特に、状態変化を複数の相手へ知らせたい場面で強力です。
signalで通知名を定義する。connect()で受信側の関数を登録する。emit()で「できごとが起きた」と知らせる。- 相手を知って命令したいなら直接呼ぶ。
- 誰が聞くかを知らずに通知したいならシグナルを使う。
迷ったときは、 「これは命令か、通知か?」 で考えてください。命令なら直接呼び出し、通知ならシグナル。この分け方を覚えるだけで、Godotのシーン構造はかなり壊れにくくなります。
次に読むなら、シーンの分け方を整理する 「シーン」と「ノード」の基本 と、処理を待つ書き方を扱う awaitとコルーチンの基礎 がつながりやすいです。