CommunityCoding & Developmentgithub.com

ziva-sh/godot-signals

Godot 4 signal patterns and their failure modes - declaring typed signals, emit(), connect() and disconnect() with Callables, bind() and unbind(), lambdas, CONNECT_ONE_SHOT and CONNECT_DEFERRED, await on signals and timers, editor-made connections stored in .tscn files, and the call down, signal up rule. Use when wiring UI buttons, timers, Area2D or body events and custom game events, when replacing Godot 3 connect("signal", self, "method") or yield code, or when debugging "already connected", "

What is godot-signals?

godot-signals is a Claude Code agent skill that godot 4 signal patterns and their failure modes - declaring typed signals, emit(), connect() and disconnect() with Callables, bind() and unbind(), lambdas, CONNECT_ONE_SHOT and CONNECT_DEFERRED, await on signals and timers, editor-made connections stored in .tscn files, and the call down, signal up rule. Use when wiring UI buttons, timers, Area2D or body events and custom game events, when replacing Godot 3 connect("signal", self, "method") or yield code, or when debugging "already connected", ".

Works with~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/ziva-sh/godot-skills/tree/HEAD/skills/godot-signals

Installed? Explore more Coding & Development skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Ask in your favorite AI

Open a new chat with this agent skill pre-loaded.

Documentation

Signals in Godot 4

In Godot 4 a signal is a first-class Signal value and a method reference is a Callable. Use the object syntax (button.pressed.connect(_on_pressed)) everywhere. The parser then rejects a misspelled handler, and a misspelled signal on self; a misspelled signal on another typed node is caught when unsafe_property_access is set to Error (see the godot-4-api-pitfalls skill). The string forms (connect("pressed", ...), emit_signal("died")) are only checked at runtime.

Declare, emit, connect

extends Node

signal health_changed(old_value: int, new_value: int)
signal died

var health := 100


func take_damage(amount: int) -> void:
	var old := health
	health = maxi(health - amount, 0)
	health_changed.emit(old, health)
	if health == 0:
		died.emit()
extends Control

@onready var start_button: Button = %StartButton
@onready var quit_button: Button = %QuitButton


func _ready() -> void:
	start_button.pressed.connect(_on_start_pressed)
	quit_button.pressed.connect(get_tree().quit)
	start_button.mouse_entered.connect(func() -> void: start_button.grab_focus())


func _on_start_pressed() -> void:
	get_tree().change_scene_to_file("res://level_1.tscn")

The handler must accept exactly the arguments the signal emits. A mismatch is not a parse error; at emit time Godot logs Error calling from signal 'x' to callable: '...': Method expected 0 argument(s), but called with 1. and skips the handler.

bind() and unbind()

  • bind(a, b) appends extra arguments after the signal's own arguments. A handler for pressed bound with one value takes one parameter; for health_changed(old, new) bound with one value it takes three, in the order old, new, bound.
  • unbind(n) drops the last n signal arguments, so a no-argument function can handle a signal that sends some.
extends HBoxContainer

signal slot_chosen(index: int)

var slot_buttons: Array[Button] = []


func _ready() -> void:
	for i in 4:
		var button := Button.new()
		button.text = "Slot %d" % (i + 1)
		add_child(button)
		slot_buttons.append(button)
		button.pressed.connect(_on_slot_pressed.bind(i))
	var timer := Timer.new()
	add_child(timer)
	timer.timeout.connect(_refresh)
	slot_chosen.connect(_refresh.unbind(1))
	timer.start(1.0)


func _on_slot_pressed(index: int) -> void:
	slot_chosen.emit(index)


func _refresh() -> void:
	for button in slot_buttons:
		button.disabled = false

Disconnect, duplicates and one-shot connections

  • disconnect() needs a Callable equal to the connected one. _handler.bind(1) built again compares equal and works. A lambda does not: keep it in a variable and disconnect that variable.
  • Connecting the same Callable twice logs Signal 'x' is already connected to given callable ... and keeps one connection. Guard with is_connected() when code can run twice, for example in _ready() of a node that is re-added to the tree.
  • connect(callable, CONNECT_ONE_SHOT) disconnects after the first emission. CONNECT_DEFERRED runs the handler at idle time. Use it (or set_deferred()) when a physics callback such as body_entered changes physics state; doing it directly logs Can't change this state while flushing queries.
  • Connections to an object are removed when that object is freed.
extends Area2D

@onready var shape: CollisionShape2D = $CollisionShape2D
var _on_first_touch := func(body: Node2D) -> void: print(body.name, " touched first")


func _ready() -> void:
	body_entered.connect(_on_first_touch, CONNECT_ONE_SHOT)
	body_entered.connect(_close_after_entry, CONNECT_DEFERRED)
	if not body_entered.is_connected(_on_body_entered):
		body_entered.connect(_on_body_entered)


func _on_body_entered(body: Node2D) -> void:
	print(body.name, " entered")


func _close_after_entry(_body: Node2D) -> void:
	shape.disabled = true


func stop_listening() -> void:
	body_entered.disconnect(_on_body_entered)

await

await signal pauses the function until the signal fires and returns its arguments: null for no arguments, the value for one, an Array for two or more. A function that contains await is a coroutine; callers that need its result must await it too, otherwise they continue immediately.

extends Node2D

signal door_opened(door_name: String)


func _ready() -> void:
	await get_tree().create_timer(0.5).timeout
	var opened_by: String = await door_opened
	print("Door opened: ", opened_by)
	await play_intro()
	print("Intro finished")


func play_intro() -> void:
	var tween := create_tween()
	tween.tween_property(self, "modulate:a", 1.0, 1.0).from(0.0)
	await tween.finished

await never times out. If the signal is never emitted (or its emitter is freed first) the function simply stops there, with no error. Use a Timer or a one-shot connection when a fallback path is needed.

Editor connections

Connections made in the Node dock are saved in the scene file, not in the script:

[connection signal="pressed" from="StartButton" to="." method="_on_start_button_pressed"]

If the method is renamed or deleted in the script, the connection stays in the .tscn. In Godot 4.7.2, emitting a signal whose connected method does not exist prints no error, so the button just stops working. After renaming a handler, search the scenes for method="old_name" and update them, or reconnect in the Node dock. Connect in code when the script should be self-contained; connect in the editor when designers wire scenes. Avoid doing both for the same signal and method: the second connect logs "already connected".

Call down, signal up

A node calls methods on its children (it owns them and knows they exist) and emits signals for its parent or siblings to react to (it should not know who is listening). The parent, or whichever node composes the two, does the connecting:

extends Node2D

@onready var player: Node2D = $Player
@onready var health_bar: ProgressBar = $HUD/HealthBar


func _ready() -> void:
	player.connect(&"health_changed", _on_player_health_changed)


func _on_player_health_changed(_old_value: int, new_value: int) -> void:
	health_bar.value = new_value

This parent only knows Player as a Node2D, so it names the custom signal as a string. Give the player script a class_name Player, type the variable as Player, and write player.health_changed.connect(_on_player_health_changed) so the parser checks the name too.

For events that many unrelated scenes care about (player died, level finished), a common pattern is an autoload script that only declares signals, used as an event bus: Events.player_died.emit() and Events.player_died.connect(_on_player_died).

Godot 3 forms to replace

Godot 3Godot 4
connect("pressed", self, "_on_pressed")pressed.connect(_on_pressed)
connect("pressed", self, "_on_pressed", [id])pressed.connect(_on_pressed.bind(id))
connect("timeout", self, "_on_timeout", [], CONNECT_ONESHOT)timeout.connect(_on_timeout, CONNECT_ONE_SHOT)
disconnect("pressed", self, "_on_pressed")pressed.disconnect(_on_pressed)
is_connected("pressed", self, "_on_pressed")pressed.is_connected(_on_pressed)
emit_signal("died")died.emit()
yield(obj, "signal_name")await obj.signal_name
funcref(obj, "method")Callable(obj, "method") or obj.method

Individual skills in this repo

This repo contains 4 individual skills — each has its own dedicated page.

ziva-sh/godot-4-api-pitfalls

Fixes Godot 3 APIs and GDScript syntax that AI assistants write into Godot 4 projects, such as KinematicBody2D, move_and_slide(velocity), export, onready, setget, yield, string connect(), instance(), rand_range, the Tween node, File, change_scene, Spatial, rect_* properties, Pool*Array, OS time functions and the old TileMap API. Use when writing, reviewing or porting GDScript for Godot 4.x, when a script fails with "not declared", "not found in base", "was removed in Godot 4" or "is not present

ziva-sh/godot-control-layout

Builds Godot 4 Control UIs that resize instead of clipping or overlapping - containers versus anchors, anchor presets from code, size flags, custom_minimum_size, MarginContainer margins, Label wrapping, mouse_filter, CanvasLayer HUDs, Godot 4 theme override names (add_theme_color_override and friends) and the display/window/stretch settings for multiple resolutions. Use when creating or fixing menus, HUDs, dialogs and inventory screens, when UI elements are cut off, zero-sized, overlapping, igno

ziva-sh/godot-scene-organization

Structures Godot 4 projects so scenes stay reusable and references do not break - when to make a scene, a script or a Resource, instancing with @export PackedScene, preload and load, @onready and %UniqueName node access, _ready order, groups, autoloads, class_name, and resource UIDs with the .uid files added in Godot 4.4, including how to move or rename files safely. Use when creating new scenes or scripts, deciding how nodes find each other, adding global state or an event bus, moving or renami

ziva-sh/godot-tilemaplayer

Correct Godot 4.3+ TileMapLayer code - set_cell(coords, source_id, atlas_coords), reading cells and custom data, erasing, coordinate conversion, terrains, tile collision through TileSet physics layers, finding the tile a body hit, and migrating from the deprecated TileMap node and its layer-index API. Use when placing, reading or erasing tiles from GDScript, building a TileSet in code, generating levels procedurally, handling tile collisions, or porting TileMap code from Godot 3 or Godot 4.0 to

Related Skills