- #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!