From c4ee1bfa0aed5b74012df1c15310fa14624156e2 Mon Sep 17 00:00:00 2001 From: Daniel Loughmiller Date: Sun, 16 Nov 2025 10:58:38 -0500 Subject: [PATCH] 1.10.0 give computed properties a manual recalculate --- Runtime/Signals/ComputedSignal.cs | 13 ++++ Tests/Editor/ComputedSignalTest.cs | 105 ++++++++++++++++++++++++----- package.json | 2 +- 3 files changed, 104 insertions(+), 16 deletions(-) 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 62807e7..34cb6ea 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "unitysignals", - "version": "1.9.1", + "version": "1.10.0", "displayName": "Unity Signals", "description": "", "unity": "2019.1",