diff --git a/com.unity.cinemachine/CHANGELOG.md b/com.unity.cinemachine/CHANGELOG.md index 7f4690691..26a8e43b6 100644 --- a/com.unity.cinemachine/CHANGELOG.md +++ b/com.unity.cinemachine/CHANGELOG.md @@ -4,6 +4,13 @@ All notable changes to this package will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [3.1.3] - 2025-12-31 + +### Bugfixes +- CameraDeactivated events were not sent consistently when a blend interrupted another blend before completion. +- CameraActivated events were not sent consistently when activation was due to timeline blends. + + ## [3.1.2] - 2024-10-01 ### Added diff --git a/com.unity.cinemachine/Runtime/Core/BlendManager.cs b/com.unity.cinemachine/Runtime/Core/BlendManager.cs index 2f2c0210d..f40541ae8 100644 --- a/com.unity.cinemachine/Runtime/Core/BlendManager.cs +++ b/com.unity.cinemachine/Runtime/Core/BlendManager.cs @@ -13,12 +13,20 @@ class BlendManager : CameraBlendStack // Current blend State - result of all frames. Blend camB is "current" camera always CinemachineBlend m_CurrentLiveCameras = new (); - // Blend state last frame, used for computing deltas - CinemachineBlend m_PreviousLiveCameras = new (); - // This is to control GC allocs when generating camera deactivated events - List m_CameraCache = new(); + HashSet m_PreviousLiveCameras = new(); + ICinemachineCamera m_PreviousActiveCamera; + bool m_WasBlending; + /// + public override void OnEnable() + { + base.OnEnable(); + m_PreviousLiveCameras.Clear(); + m_PreviousActiveCamera = null; + m_WasBlending = false; + } + /// Get the current active virtual camera. public ICinemachineCamera ActiveVirtualCamera => DeepCamBFromBlend(m_CurrentLiveCameras); @@ -102,12 +110,10 @@ public bool IsLiveInBlend(ICinemachineCamera cam) /// /// Compute the current blend, taking into account - /// the in-game camera and all the active overrides. Caller may optionally - /// exclude n topmost overrides. + /// the in-game camera and all the active overrides. /// public void ComputeCurrentBlend() { - m_PreviousLiveCameras.CopyFrom(m_CurrentLiveCameras); ProcessOverrideFrames(ref m_CurrentLiveCameras, 0); } @@ -129,25 +135,29 @@ public void RefreshCurrentCameraState(Vector3 up, float deltaTime) public ICinemachineCamera ProcessActiveCamera(ICinemachineMixer mixer, Vector3 up, float deltaTime) { // Send deactivation events - m_CameraCache.Clear(); - CollectLiveCameras(m_PreviousLiveCameras, ref m_CameraCache); - for (int i = 0; i < m_CameraCache.Count; ++i) - if (!IsLive(m_CameraCache[i])) - CinemachineCore.CameraDeactivatedEvent.Invoke(mixer, m_CameraCache[i]); + using (var enumerator = m_PreviousLiveCameras.GetEnumerator()) + { + while (enumerator.MoveNext()) + { + var item = enumerator.Current; + if (!IsLive(item)) + CinemachineCore.CameraDeactivatedEvent.Invoke(mixer, item); + } + } // Process newly activated cameras var incomingCamera = ActiveVirtualCamera; if (incomingCamera != null && incomingCamera.IsValid) { // Has the current camera changed this frame? - var outgoingCamera = DeepCamBFromBlend(m_PreviousLiveCameras); + var outgoingCamera = m_PreviousActiveCamera; if (outgoingCamera != null && !outgoingCamera.IsValid) outgoingCamera = null; // object was deleted if (incomingCamera == outgoingCamera) { - // Send a blend completeed event if appropriate - if (m_PreviousLiveCameras.CamA != null && m_CurrentLiveCameras.CamA == null) + // Send a blend completed event if appropriate + if (m_WasBlending && m_CurrentLiveCameras.CamA == null) CinemachineCore.BlendFinishedEvent.Invoke(mixer, incomingCamera); } else @@ -176,10 +186,15 @@ public ICinemachineCamera ProcessActiveCamera(ICinemachineMixer mixer, Vector3 u incomingCamera.UpdateCameraState(up, deltaTime); } } - return incomingCamera; + + // Collect cameras that are live this frame, for processing next frame + m_PreviousLiveCameras.Clear(); + CollectLiveCameras(m_CurrentLiveCameras, ref m_PreviousLiveCameras); + m_PreviousActiveCamera = DeepCamBFromBlend(m_CurrentLiveCameras); + m_WasBlending = m_CurrentLiveCameras.CamA != null; // local method - find all the live cameras in a blend - static void CollectLiveCameras(CinemachineBlend blend, ref List cams) + static void CollectLiveCameras(CinemachineBlend blend, ref HashSet cams) { if (blend.CamA is NestedBlendSource a && a.Blend != null) CollectLiveCameras(a.Blend, ref cams); @@ -191,6 +206,8 @@ static void CollectLiveCameras(CinemachineBlend blend, ref ListCall this when object is enabled - public void OnEnable() + public virtual void OnEnable() { // Make sure there is a first stack frame m_FrameStack.Clear(); @@ -184,7 +184,7 @@ public void OnEnable() } /// Call this when object is disabled - public void OnDisable() + public virtual void OnDisable() { m_FrameStack.Clear(); m_NextFrameId = 0; diff --git a/com.unity.cinemachine/Runtime/Helpers/CinemachineCameraEvents.cs b/com.unity.cinemachine/Runtime/Helpers/CinemachineCameraEvents.cs index 437caa9d3..42e407af6 100644 --- a/com.unity.cinemachine/Runtime/Helpers/CinemachineCameraEvents.cs +++ b/com.unity.cinemachine/Runtime/Helpers/CinemachineCameraEvents.cs @@ -81,7 +81,7 @@ void OnCameraActivated(ICinemachineCamera.ActivationEventParams evt) void OnBlendCreated(CinemachineCore.BlendEventParams evt) { - if (evt.Blend.CamA == (ICinemachineCamera)EventTarget || evt.Blend.CamB == (ICinemachineCamera)EventTarget) + if (evt.Blend.CamB == (ICinemachineCamera)EventTarget) BlendCreatedEvent.Invoke(evt); } diff --git a/com.unity.cinemachine/Tests/Editor/BlendManagerTests.cs b/com.unity.cinemachine/Tests/Editor/BlendManagerTests.cs index d0ea39e58..22ef675cc 100644 --- a/com.unity.cinemachine/Tests/Editor/BlendManagerTests.cs +++ b/com.unity.cinemachine/Tests/Editor/BlendManagerTests.cs @@ -29,6 +29,7 @@ public FakeMixer(string name) : base(name) {} FakeMixer m_Mixer = new ("Mixer"); FakeCamera m_Cam1 = new ("Cam1"); FakeCamera m_Cam2 = new ("Cam2"); + FakeCamera m_Cam3 = new ("Cam3"); int m_ActivatedEventCount; int m_DeactivatedEventCount; @@ -57,9 +58,18 @@ [TearDown] public void TearDown() void ResetCounters() => m_ActivatedEventCount = m_DeactivatedEventCount = m_BlendCreatedCount = m_BlendFinishedCount = 0; - void ProcessFrame(ICinemachineCamera cam, float deltaTime) + void Reset(float blendTime) { - m_BlendManager.UpdateRootFrame(m_Mixer, cam, Vector3.up, deltaTime); + m_BlendManager.LookupBlendDelegate = (outgoing, incoming) + => new (CinemachineBlendDefinition.Styles.EaseInOut, blendTime); // constant blend time + m_BlendManager.OnEnable(); + ProcessFrame(null, 0.1f); + ResetCounters(); + } + + void ProcessFrame(ICinemachineCamera activeCam, float deltaTime) + { + m_BlendManager.UpdateRootFrame(m_Mixer, activeCam, Vector3.up, deltaTime); m_BlendManager.ComputeCurrentBlend(); m_BlendManager.ProcessActiveCamera(m_Mixer, Vector3.up, deltaTime); } @@ -67,11 +77,7 @@ void ProcessFrame(ICinemachineCamera cam, float deltaTime) [Test] public void TestEvents() { - m_BlendManager.LookupBlendDelegate = (outgoing, incoming) - => new (CinemachineBlendDefinition.Styles.EaseInOut, 1); // constant blend time of 1 - - ResetCounters(); - m_BlendManager.ResetRootFrame(); + Reset(1); // constant blend time of 1 // We should get an initial activation event, no blend ProcessFrame(m_Cam1, 0.1f); @@ -110,5 +116,113 @@ public void TestEvents() Assert.AreEqual(1, m_BlendFinishedCount); Assert.That(m_BlendManager.IsBlending, Is.False); } + + [Test] + public void TestEventsNestedBlend() + { + Reset(1); // constant blend time of 1 + + // We should get an initial activation event, no blend + ProcessFrame(m_Cam1, 0.1f); + Assert.AreEqual(1, m_ActivatedEventCount); + Assert.AreEqual(0, m_DeactivatedEventCount); + Assert.AreEqual(0, m_BlendFinishedCount); + Assert.AreEqual(0, m_BlendCreatedCount); + Assert.That(m_BlendManager.IsBlending, Is.False); + + ProcessFrame(m_Cam1, 0.1f); + Assert.AreEqual(1, m_ActivatedEventCount); + Assert.AreEqual(0, m_DeactivatedEventCount); + Assert.AreEqual(0, m_BlendCreatedCount); + Assert.AreEqual(0, m_BlendFinishedCount); + Assert.That(m_BlendManager.IsBlending, Is.False); + + // Activate new camera, blend will take 1 sec + ProcessFrame(m_Cam2, 0.1f); + Assert.AreEqual(2, m_ActivatedEventCount); + Assert.AreEqual(0, m_DeactivatedEventCount); + Assert.AreEqual(1, m_BlendCreatedCount); + Assert.AreEqual(0, m_BlendFinishedCount); + Assert.That(m_BlendManager.IsBlending, Is.True); + + ProcessFrame(m_Cam2, 0.5f); + Assert.AreEqual(2, m_ActivatedEventCount); + Assert.AreEqual(0, m_DeactivatedEventCount); + Assert.AreEqual(1, m_BlendCreatedCount); + Assert.AreEqual(0, m_BlendFinishedCount); + Assert.That(m_BlendManager.IsBlending, Is.True); + + // Acivate new cam before old blend is finished + ProcessFrame(m_Cam3, 0.1f); + Assert.AreEqual(3, m_ActivatedEventCount); + Assert.AreEqual(0, m_DeactivatedEventCount); + Assert.AreEqual(2, m_BlendCreatedCount); + Assert.AreEqual(0, m_BlendFinishedCount); + Assert.That(m_BlendManager.IsBlending, Is.True); + + // After first blend time has elapsed, check the counters + ProcessFrame(m_Cam3, 0.5f); + Assert.AreEqual(3, m_ActivatedEventCount); + Assert.AreEqual(1, m_DeactivatedEventCount); + Assert.AreEqual(2, m_BlendCreatedCount); + Assert.AreEqual(0, m_BlendFinishedCount); // blend was interrupted, never finished + Assert.That(m_BlendManager.IsBlending, Is.True); + + // After second blend is finished, check the counters + ProcessFrame(m_Cam3, 0.5f); + Assert.AreEqual(3, m_ActivatedEventCount); + Assert.AreEqual(2, m_DeactivatedEventCount); + Assert.AreEqual(2, m_BlendCreatedCount); + Assert.AreEqual(1, m_BlendFinishedCount); + Assert.That(m_BlendManager.IsBlending, Is.False); + } + + [Test] + public void TestEventsBlendToNestedBlend() + { + var customBlend = new NestedBlendSource(new CinemachineBlend() + { + CamA = m_Cam1, + CamB = m_Cam2, + BlendCurve = AnimationCurve.Linear(0, 0, 1, 1), + Duration = 1, + TimeInBlend = 0.1f + }); + + Reset(1); // constant blend time of 1 + + // We should get an initial activation event, no blend + ProcessFrame(m_Cam1, 0.1f); + Assert.AreEqual(1, m_ActivatedEventCount); + Assert.AreEqual(0, m_DeactivatedEventCount); + Assert.AreEqual(0, m_BlendFinishedCount); + Assert.AreEqual(0, m_BlendCreatedCount); + Assert.That(m_BlendManager.IsBlending, Is.False); + + // Activate nested blend camera, blend will take 1 sec + ProcessFrame(customBlend, 0.1f); + Assert.AreEqual(2, m_ActivatedEventCount); + Assert.AreEqual(0, m_DeactivatedEventCount); + Assert.AreEqual(1, m_BlendCreatedCount); + Assert.AreEqual(0, m_BlendFinishedCount); + Assert.That(m_BlendManager.IsBlending, Is.True); + + // change camera in the custom blend - we expect activation and deactivation events + customBlend.Blend.CamB = m_Cam3; + ProcessFrame(customBlend, 0.1f); + Assert.AreEqual(3, m_ActivatedEventCount); + Assert.AreEqual(1, m_DeactivatedEventCount); + Assert.AreEqual(1, m_BlendCreatedCount); + Assert.AreEqual(0, m_BlendFinishedCount); + Assert.That(m_BlendManager.IsBlending, Is.True); + + customBlend.Blend.CamA = null; + ProcessFrame(customBlend, 1); + Assert.AreEqual(3, m_ActivatedEventCount); + Assert.AreEqual(2, m_DeactivatedEventCount); + Assert.AreEqual(1, m_BlendCreatedCount); + Assert.AreEqual(1, m_BlendFinishedCount); + Assert.That(m_BlendManager.IsBlending, Is.False); + } } } \ No newline at end of file diff --git a/com.unity.cinemachine/package.json b/com.unity.cinemachine/package.json index 3e65a92dd..75719c36a 100644 --- a/com.unity.cinemachine/package.json +++ b/com.unity.cinemachine/package.json @@ -1,7 +1,7 @@ { "name": "com.unity.cinemachine", "displayName": "Cinemachine", - "version": "3.1.2", + "version": "3.1.3", "unity": "2022.3", "description": "Smart camera tools for passionate creators. \n\nCinemachine 3 is a newer and better version of Cinemachine, but upgrading an existing project from 2.X will likely require some effort. If you're considering upgrading an older project, please see our upgrade guide in the user manual.", "keywords": [