The system provides a robust foundation for type-safe, event-driven communication between components while maintaining clean memory management and thread safety.
Project Settings
Swap Signal On Same Id
If a signal is created with same Id, existed one will be replaced by new one. Otherwise exception throw.
default: false
Signal Getter Timeout
Getter will be retry automatically until timeout.
default: 1.5s
Why Use It
Consider a UI component that displays a player's life: traditionally, this would require direct references to the Player or Life components. However, such tight coupling makes the UI component less reusable and more difficult to maintain. With Signal, the UI component simply subscribes to life-related signals, making it project-agnostic and highly reusable.
While there are numerous dependency injection and reference management solutions available, many of these packages introduce unnecessary complexity. Unlike engines such as Godot that have built-in signal systems, Unity's traditional GameObject-Component architecture doesn't include a native signal system. However, Unity's flexible architecture allows us to implement custom solutions, which is where this Signal system comes in.
Signals can be created and accessed from anywhere in your codebase, enabling flexible and decoupled system design. This flexibility does come with a responsibility: while powerful, signals should be used thoughtfully and systematically to avoid creating complex, hard-to-debug signal chains.
Key Benefits:
Decoupled component communication
Improved code reusability
Lightweight implementation
Flexible system design
How It Works
Signal provides a robust event-driven value management system with built-in observer pattern support and type safety.
// Create a signal using builder pattern
var signal = Signal<int>.Builder()
.WithId("PlayerLife") // optional
.WithValue(100) // optional
.WithListener(health => UpdateHealthUI(health)) // optional
.Build();
// Access and modify value
signal.Value = 75; // Automatically notifies listeners
// Get an exist signal
var lifeSignal = Signal<int>.Get("PlayerLife");
var readonlyLife = Signal<int>.GetReadonly("PlayerLife");
// Get an signal but not confirm wheather created yet
var lifeSignal = await Signal<int>.GetAsync("PlayerLife");
var readonlyLife = await Signal<int>.GetReadonlyAsync("PlayerLife");
Key Features:
Builder Pattern
Fluent interface for signal creation
Optional ID assignment (string or enum)
Initial value setting
Listener registration during creation
Readonly flag for observer protection
Value Management
Type-safe value storage and modification
Automatic listener notification on value changes
Implicit conversion to value type
Value comparison support
Observer System
Add/remove listeners dynamically
Optional immediate execution on listener addition
Thread-safe observer notifications
Protection against null listeners
Signal Registry
Global signal management
Async getter with timeout protection
Type validation
Automatic cleanup through disposal pattern
Recommended Usage
n any system, regardless of its size, proper signal management is crucial for maintaining code clarity and preventing runtime issues. Even in small applications, you'll likely find yourself handling dozens of signals across multiple objects, making a well-organized structure essential.
Below is our recommended approach to signal management, which we use in production. This pattern offers several advantages:
Type Safety: Using enums for signal IDs prevents typos and enables IDE auto-completion
State Management: Integration with Promise/Task utilities ensures safe asynchronous operations and prevents common runtime issues like timing problems during initialization
Code Organization: Clear structural patterns make the system easy to maintain and scale
While this is our recommended approach, feel free to adapt it to your specific needs. The following example demonstrates how we structure signal systems in our own projects.