Godot Signals in Unity: UnityEvents vs. C# Actions Guide
09 Aug 26 (1mo ago)
The short answer: Unity’s direct equivalents to Godot Signals are UnityEvents (for Inspector-based drag-and-drop setups) and C# Actions (for performant, code-to-code communication).
1. Quick Comparison
| Feature | Godot (GDScript) | Unity (C# Action) | UnityEvent |
|---|---|---|---|
| Declaration | signal door_opened | public event Action OnDoor; | public UnityEvent OnDoor; |
| Emission | door_opened.emit() | OnDoor?.Invoke(); | OnDoor.Invoke(); |
| Connection | .connect(_on_door) | += MyFunction; | Inspector Drag & Drop |
| Disconnection | Auto-cleaned on free | -= MyFunction; | N/A (Handled in UI) |
| Best For | Everything | High-performance gameplay logic | UI and Level Designers |
2. The Critical "Gotcha": Memory Leaks
In Godot, if a node listening to a signal is freed (queue_free), the engine automatically severs the connection. Unity does not do this.
If you subscribe to a C# event (+=) and the listening object is destroyed, the active publisher still holds a reference to that destroyed object. You must explicitly unsubscribe (-=)—typically in Unity's OnDisable() or OnDestroy() methods—to prevent dangling references, unintended callbacks, and memory leaks.
3. Global Architecture: ScriptableObjects vs. AutoLoads
While Godot relies heavily on AutoLoads (Singletons) for global signal routing, modern Unity architecture favors ScriptableObject Event Channels. This allows completely decoupled global communication (e.g., a "Game Channel") without establishing direct script-to-script dependencies.
4. Code Example: C# Action Pattern
Here is the cleanest Unity C# pattern for a standard Godot-style signal setup.
using System;
using UnityEngine;
// THE PUBLISHER (Emits the signal)
public class Player : MonoBehaviour
{
// Declare the event
public static event Action OnPlayerDeath;
void Die()
{
// Emit safely using the null-conditional operator
OnPlayerDeath?.Invoke();
}
}
// THE SUBSCRIBER (Listens to the signal)
public class UIManager : MonoBehaviour
{
// Connect when enabled
void OnEnable()
{
Player.OnPlayerDeath += ShowGameOver;
}
// DISCONNECT when disabled (CRITICAL to prevent leaks)
void OnDisable()
{
Player.OnPlayerDeath -= ShowGameOver;
}
void ShowGameOver()
{
Debug.Log("Game Over Screen Shown!");
}
}