A powerful signal system for Godot Engine that provides efficient signal handling with support for filtering, mapping, centralized event management, and high-performance signal processing.
Here are some planned features and improvements:
- π Broker Binding: Possibility to bind to the broker and receive a GSignal instance
- β‘ Extended Operations: Add more operations to GSignal such as debounce, merge, and more
- π Suggest a feature by opening an issue on GitHub!
- π Signal Processing: Create signal processing chains with filter and map operations
- π‘ Signal Broker: Connect signals across your game without direct object references
- β‘ Smart Connection Types: Automatically optimizes for high or low frequency signals
- π Pattern Matching: Subscribe to signals using wildcard patterns
- π·οΈ Alias System: Identify objects by name, groups, or custom aliases
- π Optimized Performance: Reduced overhead and minimal allocations for efficient processing
Create powerful signal processing chains with just a few lines of code:
# Filter, transform, and react to signals
GSignals.from(damage_signal)
.filter(func(amount: int) -> bool: return amount >= 10)
.map(func(amount: int) -> int: return amount * critical_multiplier)
.bind(func(final_damage: int): apply_damage(final_damage))
Connect components without direct references:
# Broadcasting side: Register player node's signals with the broker
GBroker.broadcast_signals_of(player_node, "player")
# Listening side: Subscribe to player damage events anywhere in your code
GBroker.subscribe("player:damage_taken", func(amount): update_health_ui(amount))
- Download or clone this repository
- Copy the
addons/godot-signals
folder into your project'saddons
directory - Enable the plugin in Project Settings > Plugins
# Connect to a signal with a simple callback
GSignals.from(player.health_changed)
.bind(func(new_health): update_health_bar(new_health))
# Only process signals where the value meets certain criteria
GSignals.from(enemy.attack)
.filter(func(damage: int) -> bool: return damage > 5)
.bind(func(damage): play_heavy_hit_sound())
# Transform signal data before processing
GSignals.from(position_changed)
.map(func(pos: Vector2) -> float: return pos.distance_to(Vector2.ZERO))
.bind(func(distance: float): set_volume(100 - distance))
# Delay signal processing by a specific time
GSignals.from(damage_taken)
.delay(0.5) # Delay by 0.5 seconds
.bind(func(amount): play_delayed_damage_effect(amount))
# Debounce rapid signal emissions
GSignals.from(mouse_moved)
.debounce(0.1) # Only process after 0.1s of inactivity
.bind(func(position): update_hover_effect(position))
# Store and manage connections
var connection = GSignals.from(timer.timeout).bind(func(): spawn_enemy())
# Temporarily disable connection
connection.stop()
# Re-enable connection
connection.start()
# Register an object's signals with the broker
GBroker.broadcast_signals_of(
player, # The object whose signals will be broadcasted
"player", # Alias for identifying the object (optional)
GBroker.GBrokerBroadcastFlags.SCRIPT_ONLY # Which signals to broadcast
)
# Register with multiple aliases for more flexible subscription patterns
GBroker.broadcast_signals_of(
boss_entity,
["boss", "enemy", "entity"]
)
# Subscribe to specific signals
GBroker.subscribe("player:health_changed", func(new_health): update_health_ui(new_health))
# Use wildcard patterns to subscribe to multiple signals
GBroker.subscribe("player:*", func(emitter, signal_name, args): print("Player signal: ", signal_name))
GBroker.subscribe("*:damage_taken", func(damage): update_global_damage_counter(damage))
# Clear all subscriptions and signal handlers when switching scenes
func _exit_tree():
GBroker.reset()
The broker intelligently handles callback arguments depending on how many parameters your callback function accepts:
# Just receive the signal arguments (most common)
GBroker.subscribe("player:health_changed", func(health_amount): update_ui(health_amount))
# Receive emitter object, signal name, and arguments
GBroker.subscribe("player:*", func(emitter, signal_name, args):
print("Signal %s from %s with args %s" % [signal_name, emitter.name, args])
)
# Mixed patterns with different argument counts
GBroker.subscribe("enemy:hit", func(damage, hit_position):
spawn_particle(hit_position)
apply_damage(damage)
)
# Missing arguments are filled with null
GBroker.subscribe("*:*", func(emitter, signal_name, arg1, arg2, arg3):
# arg2 and arg3 will be null if the signal emits less than 3 arguments
print("%s emitted %s with up to 3 args: %s, %s, %s" % [
emitter.name, signal_name, arg1, arg2, arg3
])
)
- If the callback has fewer parameters than the signal provides, extra signal arguments are ignored
- If the callback has more parameters than the signal provides, extra callback parameters receive null
- The first callback parameter can receive the emitter object if an extra parameter is available
- The second parameter can receive the signal name if additional parameters are available
- Subsequent parameters receive the signal arguments
# Optimize for high-frequency signals (e.g., position updates)
GSignals.from(position_changed, GSignals.GSignalsBindFlags.HIGH_FREQUENCY_HINT)
When no alias is provided, the broker automatically uses:
- π₯ The node's groups (except internal groups)
- π The node's name
- π€ Snake case version of the node's name
# For a node named "PlayerCharacter" in group "characters"
GBroker.broadcast_signals_of(player_node)
# This will automatically register with aliases:
# - "characters" (from group)
# - "PlayerCharacter" (from name)
# - "player_character" (snake case conversion)
- π Chain operations carefully as each adds processing overhead
- ποΈ Use the
HIGH_FREQUENCY_HINT
flag for signals that emit multiple times per frame - π§ The pattern matching system uses caching to optimize frequent signal matches
- π Direct signal connections with GSignals are more efficient than broker subscriptions when you have direct references
This project is licensed under the MIT License - see the LICENSE file for details.