diff --git a/com.unity.cinemachine/CHANGELOG.md b/com.unity.cinemachine/CHANGELOG.md index 1e3a55d21..c144d49fd 100644 --- a/com.unity.cinemachine/CHANGELOG.md +++ b/com.unity.cinemachine/CHANGELOG.md @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [3.1.7-pre.1] - 2026-02-24 +### Bugfixes + - Fixed blend cancellation not taking into account updates to "cancelled" camera state + ### Changed - Added support for fast enter play mode. diff --git a/com.unity.cinemachine/Runtime/Core/CameraBlendStack.cs b/com.unity.cinemachine/Runtime/Core/CameraBlendStack.cs index a6431f861..a52d38ff2 100644 --- a/com.unity.cinemachine/Runtime/Core/CameraBlendStack.cs +++ b/com.unity.cinemachine/Runtime/Core/CameraBlendStack.cs @@ -294,7 +294,6 @@ public void UpdateRootFrame( // to cancel out the progress made in the opposite direction if (backingOutOfBlend) { - snapshot = true; // always use a snapshot for this to prevent pops duration = duration * normalizedBlendPosition; // skip first part of blend normalizedBlendPosition = 1 - normalizedBlendPosition; } @@ -306,7 +305,11 @@ public void UpdateRootFrame( { var blendCopy = new CinemachineBlend(); blendCopy.CopyFrom(frame.Blend); - camA = new NestedBlendSource(blendCopy); + + if (backingOutOfBlend) + camA = new FrozenBlendSource(blendCopy); + else + camA = new NestedBlendSource(blendCopy); } } // For the event, we use the raw outgoing camera, not the actual blend source @@ -328,27 +331,31 @@ public void UpdateRootFrame( } // Advance the working blend - if (AdvanceBlend(frame.Blend, deltaTime)) + if (AdvanceBlend(frame, deltaTime)) frame.Source.ClearBlend(); frame.UpdateCameraState(up, deltaTime); // local function - static bool AdvanceBlend(CinemachineBlend blend, float deltaTime) + static bool AdvanceBlend(NestedBlendSource source, float deltaTime) { - bool blendCompleted = false; - if (blend.CamA != null) - { + var blend = source.Blend; + if (blend.CamA == null) + return false; + + if (source is not FrozenBlendSource) blend.TimeInBlend += (deltaTime >= 0) ? deltaTime : blend.Duration; - if (blend.IsComplete) - { - // No more blend - blend.ClearBlend(); - blendCompleted = true; - } - else if (blend.CamA is NestedBlendSource bs) - AdvanceBlend(bs.Blend, deltaTime); + + if (blend.IsComplete) + { + // No more blend + blend.ClearBlend(); + return true; } - return blendCompleted; + + if (blend.CamA is NestedBlendSource bs) + AdvanceBlend(bs, deltaTime); + + return false; } } @@ -471,5 +478,15 @@ public void TakeSnapshot(ICinemachineCamera source) m_Name ??= source == null ? "(null)" : "*" + source.Name; } } + + /// + /// Source for a blend where the time is frozen. Used for cancelling blends where we want + /// the source of the cancelling blend to be at the point in time when the blend was cancelled, + /// but still update the undelying sources in case they move. + /// + class FrozenBlendSource : NestedBlendSource + { + public FrozenBlendSource(CinemachineBlend blend) : base(blend) {} + } } } diff --git a/com.unity.cinemachine/Tests/Editor/BlendManagerTests.cs b/com.unity.cinemachine/Tests/Editor/BlendManagerTests.cs index 989ef031b..3f86679bd 100644 --- a/com.unity.cinemachine/Tests/Editor/BlendManagerTests.cs +++ b/com.unity.cinemachine/Tests/Editor/BlendManagerTests.cs @@ -279,5 +279,34 @@ public void TestBlendReversal() Assert.That(m_BlendManager.IsBlending, Is.False); Assert.AreEqual(1.0f, m_BlendManager.CameraState.RawPosition.x, 0.001f); } + + [Test] + public void TestBlendCancellationKeepsOutgoingCameraUpdating() + { + void MoveCameras() { + m_Cam1.Position += Vector3.forward; + m_Cam2.Position += Vector3.forward; + } + + Reset(1); // constant blend time of 1 + m_Cam1.Position = new Vector3(0, 0, 0); + m_Cam2.Position = new Vector3(1, 0, 0); + + // Start with cam1, then blend towards cam2. + ProcessFrame(m_Cam1, 0.1f); + MoveCameras(); + ProcessFrame(m_Cam2, 0.5f); + MoveCameras(); + Assume.That(m_BlendManager.IsBlending, Is.True); + + // Cancel/reverse the blend back to cam1 and advance a couple of frames. + ProcessFrame(m_Cam1, 0.1f); + MoveCameras(); + ProcessFrame(m_Cam1, 0.1f); + Assume.That(m_BlendManager.IsBlending, Is.True); + + // Z coordinate should be the same for both cameras and blend result + Assert.AreEqual(m_Cam1.Position.z, m_BlendManager.CameraState.RawPosition.z, 0.001f); + } } } \ No newline at end of file