Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

1 watching

Forks

Repository files navigation

Unity Signals

A reactive signals library for Unity that provides observable values with automatic change notifications.

Features

  • Value Signals: Observable primitive and struct types with equality-based change detection
  • Reference Signals: Observable reference types with reference equality checking
  • Computed Signals: Automatically recalculate based on dependencies
  • Transactions: Batch multiple signal updates with a single notification wave
  • Multiple Observer Types: Support for delegates, actions, and observer objects
  • Type Safety: Strongly-typed signal values
  • Memory Management: Proper disposal and dead dependency tracking

Basic Usage

Creating Signals

// Value signals for primitives
var health = new IntegerValueSignal(100);
var speed = new FloatValueSignal(5.0f);
var playerName = new StringValueSignal("Player");

// Reference signals for objects
var currentWeapon = new ReferenceSignal<Weapon>();

Observing Changes

// Using delegates
health.AddObserver((sender, oldValue, newValue) => {
    Debug.Log($"Health changed from {oldValue} to {newValue}");
});

// Using actions (new value only)
health.AddObserver((newValue) => {
    UpdateHealthBar(newValue);
});

// Using events
health.SignalValueChanged += OnHealthChanged;

Setting Values

health.Value = 75;  // Using property
health.SetValue(75); // Using method

Computed Signals

var maxHealth = new IntegerValueSignal(100);
var currentHealth = new IntegerValueSignal(80);

var healthPercentage = new ComputedSignal<float>(() => 
    (float)currentHealth.Value / maxHealth.Value
);

// Automatically recalculates when dependencies change
currentHealth.Value = 50;
Debug.Log(healthPercentage.Value); // 0.5

Transactions

A transaction holds back change notifications while you update several signals, then delivers them once when the scope ends. Every signal change inside the scope counts, whether it comes from your code, a method you call, or a collection mutation.

using (SignalTransaction.Begin())
{
    position.Value = new Vector3(10, 0, 5);
    rotation.Value = 45f;
    inventory.Add(sword);
} // Observers are notified here

How Transactions Work

  1. Values change immediately: reading a signal inside the scope returns its new value, and computed signals recalculate on read.
  2. Notifications are deferred: each changed signal notifies its observers once when the outermost scope ends, in the order the signals were first changed.
  3. Original old values: observers receive the value from before the transaction and the final value.
  4. No-op changes are skipped: a signal that ends where it started (100 → 50 → 100) does not notify.
  5. Collection events replay: ItemAdded, ItemRemoved, ItemReplaced, Cleared and CollectionChanged are replayed in their original order, followed by one signal notification.
var score = new IntegerValueSignal(0);

score.AddObserver((sender, oldValue, newValue) => {
    Debug.Log($"Score: {oldValue} -> {newValue}");
});

using (SignalTransaction.Begin())
{
    score.Value = 10;
    score.Value = 20;
    score.Value = 30;

    Debug.Log(score.Value); // Prints: 30
}
// Output: "Score: 0 -> 30"

Nesting, Cascades and Errors

  • Nested scopes merge: an inner Begin() does nothing extra; everything is delivered when the outermost scope ends.
  • Cascades are queued: if an observer changes another signal during delivery, that change is delivered after the current wave finishes rather than interrupting it. Delivery gives up with an InvalidOperationException after SignalTransaction.MaxDeliveryWaves waves, which usually means observers are changing each other in a cycle.
  • Throwing observers don't block others: every pending notification is still delivered, then the exception is rethrown (an AggregateException if several observers threw).
  • No rollback: if the scope exits with an exception, the values stay changed and notifications are still delivered.
  • Disposed signals are skipped: a signal disposed before the scope ends does not notify.

ObservableList.AddRange and ReplaceAll run inside their own transaction, so signal observers are notified once per call.

Transaction with Computed Signals

var firstName = new StringValueSignal("John");
var lastName = new StringValueSignal("Doe");

var fullName = new ComputedSignal<string>(() =>
    $"{firstName.Value} {lastName.Value}"
);

fullName.AddObserver((name) => Debug.Log($"Full name: {name}"));

using (SignalTransaction.Begin())
{
    firstName.Value = "Bob";
    Debug.Log(fullName.Value); // "Bob Doe"
    lastName.Value = "Johnson";
} // Triggers once: "Full name: Bob Johnson"

Observer Management

// Remove specific observer
void OnHealthChanged(int newValue) { }
health.AddObserver(OnHealthChanged);
health.RemoveObserver(OnHealthChanged);

// Clear all observers
health.ClearObservers();

Disposal

// Properly dispose signals when done
health.Dispose();

// Disposed signals mark themselves as dead and clear observers

Available Signal Types

Basic Value Signals

  • BooleanValueSignal
  • IntegerValueSignal
  • FloatValueSignal
  • DoubleValueSignal
  • LongValueSignal
  • ByteValueSignal
  • DecimalValueSignal
  • CharValueSignal
  • StringValueSignal

Unity Type Signals

  • Vector2ValueSignal
  • Vector3ValueSignal

Generic Signals

  • ValueSignal<T> - For any value type
  • ReferenceSignal<T> - For any reference type
  • ComputedSignal<T> - For derived values

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages