UniSignal
Intuitive and flexible C# signal management library tailored for Unity, allowing seamless signal subscription and dispatch based on class type, specific data, or custom predicates
com.actionik.polymorphex.unisignal 
Install via UPM
Add to Unity Package Manager using this URL
https://www.pkglnk.dev/unisignal.git README Markdown
Copy this to your project's README.md
## Installation
Add **UniSignal** to your Unity project via Package Manager:
1. Open **Window > Package Manager**
2. Click **+** > **Add package from git URL**
3. Enter:
```
https://www.pkglnk.dev/unisignal.git
```
[](https://www.pkglnk.dev/pkg/unisignal)README
A lightweight, zero-allocation signal (event) library for Unity. Decouple your systems with type-safe, struct-based signals — no strings, no boxing, no garbage.
Features
- Type-safe signals — signals are structs, caught at compile time
- Zero GC in steady state — subscription objects are pooled and recycled automatically
- Multiple subscription modes — by type, by specific value, or by predicate
- Reentrant dispatch — subscribe/unsubscribe safely from inside a signal callback
- Listener-based bulk unsubscribe — pass
thisas listener, then unsubscribe everything in one call (great forOnDestroy) - No singletons — create as many
SignalHubinstances as you need (global, per-scene, per-entity)
Table of Contents
- Installation
- Quick Start
- Defining Signals
- Subscribing
- Dispatching
- Unsubscribing
- API Reference
- Performance
- Tests
- License
Installation
Option 1: Unity Package Manager (UPM)
- Open Window → Package Manager
- Click + → Add package from git URL...
- Enter:
https://github.com/actionk/UniSignal.git#1.0.0
Option 2: manifest.json
Add to your Packages/manifest.json:
"com.actionik.polymorphex.unisignal": "https://github.com/actionk/UniSignal.git#1.0.0"
Quick Start
// 1. Define a signal
public struct PlayerDiedSignal : ISignal { }
// 2. Create a hub (or inject one via DI)
var signalHub = new SignalHub();
// 3. Subscribe
signalHub.Subscribe<PlayerDiedSignal>(this, () =>
{
Debug.Log("Player died!");
});
// 4. Dispatch
signalHub.Dispatch(new PlayerDiedSignal());
// 5. Cleanup
signalHub.Unsubscribe(this);
Defining Signals
Simple Signals
A signal is any struct that implements ISignal. It can be completely empty if you only need to notify that something happened:
public struct GameStartedSignal : ISignal { }
public struct GameOverSignal : ISignal { }
Signals with Data
Signals can carry data. Subscribers can optionally receive it:
public struct DamageSignal : ISignal
{
public int amount;
public Entity source;
}
Matchable Signals
If you want subscribers to match on a specific signal value (not just the type), implement ISignal<T> which requires IEquatable<T>:
public struct ItemPickedUpSignal : ISignal<ItemPickedUpSignal>
{
public int itemId;
public bool Equals(ItemPickedUpSignal other) => itemId == other.itemId;
public override int GetHashCode() => itemId;
}
This enables subscribing to, for example, only item ID 42 being picked up.
Subscribing
All Subscribe overloads return a SignalSubscription<T> that can be used to unsubscribe later.
By Signal Type
Callback fires for every signal of that type.
// Without data
signalHub.Subscribe<DamageSignal>(this, () =>
{
Debug.Log("Something took damage");
});
// With data
signalHub.Subscribe<DamageSignal>(this, (DamageSignal signal) =>
{
Debug.Log($"Took {signal.amount} damage");
});
By Signal Type with Data
Same as above, but the callback receives the signal struct:
signalHub.Subscribe<DamageSignal>(this, (DamageSignal signal) =>
{
healthBar.SetValue(healthBar.Value - signal.amount);
});
By Specific Value
Only fires when the dispatched signal equals the subscribed value. Requires ISignal<T>:
// Without data
signalHub.Subscribe(this, new ItemPickedUpSignal { itemId = 42 }, () =>
{
Debug.Log("Picked up item 42!");
});
// With data
signalHub.Subscribe(this, new ItemPickedUpSignal { itemId = 42 }, (ItemPickedUpSignal signal) =>
{
Debug.Log($"Got item {signal.itemId}");
});
By Predicate
Only fires when the predicate returns true:
// Without data
signalHub.Subscribe<DamageSignal>(this,
signal => signal.amount > 50,
() => Debug.Log("Big hit!")
);
// With data
signalHub.Subscribe<DamageSignal>(this,
signal => signal.amount > 50,
(DamageSignal signal) => Debug.Log($"Big hit: {signal.amount}")
);
By Predicate with Data
signalHub.Subscribe<DamageSignal>(this,
signal => signal.source != Entity.Invalid,
(DamageSignal signal) =>
{
Debug.Log($"Damage from {signal.source}: {signal.amount}");
}
);
Without a Listener
You can omit the listener object. This is useful for static or one-off subscriptions, but you won't be able to bulk-unsubscribe by listener:
signalHub.Subscribe<GameStartedSignal>(() => Debug.Log("Game started"));
Dispatching
signalHub.Dispatch(new DamageSignal { amount = 25 });
Dispatch is reentrant — it's safe to subscribe, unsubscribe, or dispatch again from inside a callback. Changes are queued and applied after the current dispatch completes.
Unsubscribing
Single Subscription
var sub = signalHub.Subscribe<DamageSignal>(this, () => { });
// Either via the hub:
signalHub.Unsubscribe(sub);
// Or directly on the subscription:
sub.Unsubscribe();
All Subscriptions of a Listener
Removes all subscriptions (across all signal types) registered with this listener object. Ideal for OnDestroy:
signalHub.Unsubscribe(this);
All Subscriptions of a Listener for a Specific Signal
Removes only subscriptions of a specific signal type for the given listener:
signalHub.Unsubscribe<DamageSignal>(this);
All Subscriptions of a Signal Type
Removes all subscriptions of a signal type, regardless of listener:
signalHub.UnsubscribeAllFrom<DamageSignal>();
API Reference
SignalHub
| Method | Description |
|---|---|
Subscribe<T>(listener, callback) |
Subscribe to all signals of type T |
Subscribe<T>(listener, Action<T>) |
Subscribe with signal data |
Subscribe<T>(listener, predicate, callback) |
Subscribe with a filter predicate |
Subscribe<T>(listener, predicate, Action<T>) |
Subscribe with predicate + signal data |
Subscribe<T>(listener, signal, callback) |
Subscribe to a specific signal value (ISignal<T>) |
Subscribe<T>(listener, signal, Action<T>) |
Subscribe to a specific value with data |
Dispatch<T>(signal) |
Dispatch a signal to all matching subscribers |
Unsubscribe(subscription) |
Remove a single subscription |
Unsubscribe(listener) |
Remove all subscriptions for a listener |
Unsubscribe<T>(listener) |
Remove all T subscriptions for a listener |
UnsubscribeAllFrom<T>() |
Remove all subscriptions of type T |
GetSubscriptionListeners() |
Get all registered listener objects |
All Subscribe overloads are also available without the listener parameter.
ISignal
public interface ISignal { }
Marker interface for signal structs.
ISignal<T>
public interface ISignal<T> : ISignal, IEquatable<T> { }
For signals that can be matched by value using Subscribe(listener, signalValue, callback).
Performance
- Object pooling: subscription objects are pooled per signal type — no allocations after warmup
- Indexed for-loops: all internal iteration uses indexed loops to avoid enumerator allocations
- Reentrant dispatch: uses a depth counter instead of copying lists, so nested dispatches are efficient
- Struct signals: no boxing — signals stay on the stack
Tests
Unit tests are included in the Tests/ folder. Run them via Unity's Test Runner (Window → General → Test Runner).
License
Distributed under the MIT License. See LICENSE for more information.
No comments yet. Be the first!