diff --git a/Runtime/Signals/ComputedSignal.cs b/Runtime/Signals/ComputedSignal.cs
index 178d096..86a38b8 100644
--- a/Runtime/Signals/ComputedSignal.cs
+++ b/Runtime/Signals/ComputedSignal.cs
@@ -65,6 +65,19 @@ private bool HasDeadDependencies()
// Add a public method to check health explicitly
public bool HasValidDependencies() => !HasDeadDependencies();
+
+ ///
+ /// Manually triggers a recalculation of this computed signal.
+ /// Useful when the computation depends on non-reactive values.
+ ///
+ public TSignalType Recalculate()
+ {
+ RecalculateValue(notifyObservers: true, out var newValue);
+ _currentValue = newValue;
+ _isDirty = false;
+
+ return newValue;
+ }
///
/// Recalculates the value of this computed signal
diff --git a/Tests/Editor/ComputedSignalTest.cs b/Tests/Editor/ComputedSignalTest.cs
index 6d8f865..3e29afc 100644
--- a/Tests/Editor/ComputedSignalTest.cs
+++ b/Tests/Editor/ComputedSignalTest.cs
@@ -203,19 +203,17 @@ public void TestNestedComputedSignalDependencyDeath()
var baseSignal = new IntegerValueSignal(42);
var computed1 = new ComputedSignal(() => baseSignal.GetValue() * 2);
var computed2 = new ComputedSignal(() => computed1.GetValue() + 1);
-
+
bool computed1DiedEventFired = false;
- computed1.SignalDied += (sender) => {
- computed1DiedEventFired = true;
- };
-
+ computed1.SignalDied += (sender) => { computed1DiedEventFired = true; };
+
Assert.IsFalse(computed2.IsDead, "Computed signal should not be dead initially");
-
+
baseSignal.Dispose();
-
+
// Check if the event fired
Assert.IsTrue(computed1DiedEventFired, "SignalDied event should fire for computed1");
-
+
// Death should propagate through the chain
Assert.IsTrue(computed1.IsDead, "First computed signal should be dead after base signal death");
Assert.IsTrue(computed2.IsDead, "Second computed signal should be dead after dependency chain death");
@@ -257,27 +255,104 @@ public void TestComplexSignalArrangement()
{
IntegerValueSignal signalA = new(1);
- ComputedSignal computedA = new ComputedSignal(() => signalA.GetValue()+1);
- ComputedSignal computedB = new ComputedSignal(() => signalA.GetValue()*3);
-
+ ComputedSignal computedA = new ComputedSignal(() => signalA.GetValue() + 1);
+ ComputedSignal computedB = new ComputedSignal(() => signalA.GetValue() * 3);
+
ComputedSignal computedC = new ComputedSignal(() => computedB.GetValue() + computedA.GetValue());
-
+
int computedACallCount = 0;
int computedBCallCount = 0;
int computedCCallCount = 0;
-
+
computedA.AddObserver((Action)(newValue => { computedACallCount++; }));
computedB.AddObserver((Action)(newValue => { computedBCallCount++; }));
computedC.AddObserver((Action)(newValue => { computedCCallCount++; }));
-
+
Assert.AreEqual(5, computedC.GetValue(), "Initial computedC value incorrect");
signalA.Value = 5;
Assert.AreEqual(1, computedACallCount, "computedA should have been called once after signalA change");
Assert.AreEqual(1, computedBCallCount, "computedB should have been called once after signalA change");
Assert.AreEqual(1, computedCCallCount, "computedC should have been called once after signalA change");
-
+
Assert.AreEqual(21, computedC.GetValue(), "Updated computedC value incorrect");
}
+
+ [Test]
+ public void TestManualRecalculate_WithNonReactiveVariable()
+ {
+ int externalValue = 5;
+ var signal = new ComputedSignal(() => externalValue * 2);
+
+ Assert.AreEqual(10, signal.GetValue(), "Initial value should be 10");
+
+ // Change external value - signal won't know automatically
+ externalValue = 10;
+ Assert.AreEqual(10, signal.GetValue(), "Value should still be 10 (not recalculated)");
+
+ // Manual recalculate
+ int result = signal.Recalculate();
+ Assert.AreEqual(20, result, "Recalculate should return new value of 20");
+ Assert.AreEqual(20, signal.GetValue(), "GetValue should now return 20");
+ }
+
+ [Test]
+ public void TestManualRecalculate_NotifiesObservers()
+ {
+ int externalValue = 5;
+ var signal = new ComputedSignal(() => externalValue * 2);
+
+ int observerCallCount = 0;
+ int lastReceivedValue = 0;
+
+ signal.AddObserver((int newValue) => {
+ observerCallCount++;
+ lastReceivedValue = newValue;
+ });
+
+ externalValue = 10;
+ signal.Recalculate();
+
+ Assert.AreEqual(1, observerCallCount, "Observer should be called once");
+ Assert.AreEqual(20, lastReceivedValue, "Observer should receive new value of 20");
+ }
+
+ [Test]
+ public void TestManualRecalculate_NoChangeNoNotification()
+ {
+ int externalValue = 5;
+ var signal = new ComputedSignal(() => externalValue * 2);
+
+ int observerCallCount = 0;
+ signal.AddObserver((int newValue) => observerCallCount++);
+
+ // Recalculate without changing externalValue
+ int result = signal.Recalculate();
+
+ Assert.AreEqual(10, result, "Should return same value");
+ Assert.AreEqual(0, observerCallCount, "Observer should not be called when value doesn't change");
+ }
+
+ [Test]
+ public void TestManualRecalculate_MixedReactiveAndNonReactive()
+ {
+ var reactiveSignal = new IntegerValueSignal(5);
+ int nonReactiveValue = 3;
+
+ var computed = new ComputedSignal(() => reactiveSignal.GetValue() + nonReactiveValue);
+
+ Assert.AreEqual(8, computed.GetValue(), "Initial: 5 + 3 = 8");
+
+ // Change reactive signal - should auto-update
+ reactiveSignal.Value = 10;
+ Assert.AreEqual(13, computed.GetValue(), "Reactive change: 10 + 3 = 13");
+
+ // Change non-reactive value - needs manual recalculate
+ nonReactiveValue = 7;
+ Assert.AreEqual(13, computed.GetValue(), "Before recalculate: still 13");
+
+ int result = computed.Recalculate();
+ Assert.AreEqual(17, result, "After recalculate: 10 + 7 = 17");
+ }
}
}
\ No newline at end of file
diff --git a/package.json b/package.json
index ca344f5..34cb6ea 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "unitysignals",
- "version": "1.9.2",
+ "version": "1.10.0",
"displayName": "Unity Signals",
"description": "",
"unity": "2019.1",
@@ -8,4 +8,4 @@
"name": "Daniel Loughmiller",
"url": "https://github.com/DanLGameDev/"
}
-}
+}
\ No newline at end of file