> voidtick
Menu
  • #Godot
  • #GDScript
  • #Signals

Handling arguments in signals

Problem

Imagine you have a simple signal that is emitted on enemy death and you use it to update some data or update UI elements.

# event_bus.gd (autoload)

signal enemy_died
# enemy.gd

func die():
	EventBus.enemy_died.emit()
	queue_free()
# game.gd

func _ready():
	EventBus.enemy_died.connect(_on_enemy_died)

func _on_enemy_died():
	kills_count += 1

Simple, isn’t it?

For some reason you decide to pass the enemy instance to this signal to update the player’s XP, which is handled in a separate module.

# event_bus.gd (autoload)

signal enemy_died(enemy)
# enemy.gd

func die():
	EventBus.enemy_died.emit(self)  # passing the reference
	queue_free()
# xp_manager.gd

func _ready():
	EventBus.enemy_died.connect(_on_enemy_died)

func _on_enemy_died(enemy):
	player_xp += enemy.gained_xp

But our counter stopped updating. Why? Because we forgot to update the signal handler in game.gd.

Let’s fix this.

# game.gd

func _on_enemy_died(enemy):  # passing required argument to handler function
	kills_count += 1

Then, after the enemy’s death, you want to spawn some beautiful FX, but you need the position where the enemy was killed. Of course you can take this from the enemy instance, but it’s better not to rely on it as the enemy is queued for deletion and your required code in the signal handler can be called a bit later. Let’s pass the position directly.

# event_bus.gd (autoload)

signal enemy_died(enemy, position)

And now you realize the problem - you have to update ALL these signal handlers. Even if some of these handlers do not care about the new arguments (you don’t need a position to update XP, for example).


Solution

Fortunately, the solution is quite simple - just always create a separate class to carry signal data. You can add “Event” at the end of the name for readability.

# enemy_died_event.gd

class_name EnemyDiedEvent extends RefCounted

var enemy: Enemy
var position: Vector2
# event_bus.gd (autoload)

signal enemy_died(event_data: EnemyDiedEvent)
# enemy.gd

func die():
	var event := EnemyDiedEvent.new()
	event.enemy = self
	event.position = global_position
	EventBus.enemy_died.emit(event)
	queue_free()
# game.gd

func _ready():
	EventBus.enemy_died.connect(_on_enemy_died)

func _on_enemy_died(event: EnemyDiedEvent):  # the handler does not use any event data and that's ok
	kills_count += 1
# xp_manager.gd

func _ready():
	EventBus.enemy_died.connect(_on_enemy_died)

func _on_enemy_died(event: EnemyDiedEvent):
	player_xp += event.enemy.gained_xp
# fx_manager.gd

func _ready():
	EventBus.enemy_died.connect(_on_enemy_died)

func _on_enemy_died(event: EnemyDiedEvent):
	create_explosion(event.position)

Now you can freely extend your signal data without worrying about updating existing code (unless you rename or delete event data variables).

That’s it!

More Articles