【Godot】シグナルによるノード間の連携

作成: 2025-12-06最終更新: 2026-07-07

Godotのシグナルを使って、UI、サウンド、ゲーム進行を疎結合に連携させる方法を、具体的なゲーム実装例とともに解説します。

プレイヤーがダメージを受けたとき、HPバーを減らし、効果音を鳴らし、画面を揺らし、ゲームオーバー判定もしたい。Godotを始めたばかりだと、つい get_node() で全部の相手を探して直接呼びたくなります。

小さな試作ならそれでも動きます。ただ、UIの階層を変えた瞬間に壊れたり、プレイヤーを別シーンで使い回せなくなったりします。この記事では、Godotの シグナル を使って「イベントが起きたことだけを知らせる」設計に変える方法を解説します。

シグナルが複数の受信先へ通知を届けるイメージ

この記事でわかること

  • signalconnect()emit() の役割
  • UI、サウンド、ゲーム進行をプレイヤーから切り離す考え方
  • エディタ接続とコード接続の使い分け
  • 直接呼び出しとシグナルの判断基準
Sponsored


シグナルは「できごとの通知」

シグナルは、あるノードが「何かが起きた」と外へ知らせるための仕組みです。発信する側は、誰が聞いているかを知りません。聞きたい側だけが、そのシグナルに接続しておきます。

Playerがhealth_changedを発信し、HUD、GameManager、Soundが受け取る流れ

たとえば 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 をボス戦、チュートリアル、ミニゲームで再利用しようとしても、毎回同じ HUDSoundManager が必要になります。


ゲーム開発ではどこで使う?

シグナルは「1つのできごとを、複数の場所がそれぞれの責任で処理する」場面に向いています。

RPG、タワーディフェンス、パズルでシグナルを使う場面の例
  • 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()

PlayerHUDSoundGameManager も直接呼びません。「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")

この構成なら、後から DamageFlashAchievementTracker を足したくなっても、Player.gd は変更せずに済みます。受信側を1つ増やして player.died.connect(...) するだけです。


エディタ接続とコード接続

Godotでは、シグナルをエディタから接続することも、GDScriptから接続することもできます。

エディタ接続が向いている場面

Button.pressed のように、シーン内で固定されたUIをつなぐ場合はエディタ接続が分かりやすいです。

  1. シーンツリーで Button を選ぶ。
  2. 右側の「ノード」タブから pressed() を選ぶ。
  3. 接続先ノードと関数名を選ぶ。
# 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とコルーチンの基礎 がつながりやすいです。