Skip to content

Commit

Permalink
CMCL-0000: CameraDeactivated events were not sent when a blend interr…
Browse files Browse the repository at this point in the history
…upted another (#1028)

* CameraDeactivated events were not sent when a blend interrupted another blend

* fix test

* Update BlendManagerTests.cs

* Revert "Use splines 2.7.1"

This reverts commit 867c0a0.

* Fix additional missing activation events
  • Loading branch information
glabute authored Dec 11, 2024
1 parent 73ca01e commit 6c06201
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 28 deletions.
7 changes: 7 additions & 0 deletions com.unity.cinemachine/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
51 changes: 34 additions & 17 deletions com.unity.cinemachine/Runtime/Core/BlendManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ICinemachineCamera> m_CameraCache = new();
HashSet<ICinemachineCamera> m_PreviousLiveCameras = new();
ICinemachineCamera m_PreviousActiveCamera;
bool m_WasBlending;

/// <inheritdoc/>
public override void OnEnable()
{
base.OnEnable();
m_PreviousLiveCameras.Clear();
m_PreviousActiveCamera = null;
m_WasBlending = false;
}

/// <summary>Get the current active virtual camera.</summary>
public ICinemachineCamera ActiveVirtualCamera => DeepCamBFromBlend(m_CurrentLiveCameras);

Expand Down Expand Up @@ -102,12 +110,10 @@ public bool IsLiveInBlend(ICinemachineCamera cam)

/// <summary>
/// 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.
/// </summary>
public void ComputeCurrentBlend()
{
m_PreviousLiveCameras.CopyFrom(m_CurrentLiveCameras);
ProcessOverrideFrames(ref m_CurrentLiveCameras, 0);
}

Expand All @@ -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
Expand Down Expand Up @@ -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<ICinemachineCamera> cams)
static void CollectLiveCameras(CinemachineBlend blend, ref HashSet<ICinemachineCamera> cams)
{
if (blend.CamA is NestedBlendSource a && a.Blend != null)
CollectLiveCameras(a.Blend, ref cams);
Expand All @@ -191,6 +206,8 @@ static void CollectLiveCameras(CinemachineBlend blend, ref List<ICinemachineCame
else if (blend.CamB != null)
cams.Add(blend.CamB);
}

return incomingCamera;
}
}
}
4 changes: 2 additions & 2 deletions com.unity.cinemachine/Runtime/Core/CameraBlendStack.cs
Original file line number Diff line number Diff line change
Expand Up @@ -176,15 +176,15 @@ public void ReleaseCameraOverride(int overrideId)
}

/// <summary>Call this when object is enabled</summary>
public void OnEnable()
public virtual void OnEnable()
{
// Make sure there is a first stack frame
m_FrameStack.Clear();
m_FrameStack.Add(new StackFrame());
}

/// <summary>Call this when object is disabled</summary>
public void OnDisable()
public virtual void OnDisable()
{
m_FrameStack.Clear();
m_NextFrameId = 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
128 changes: 121 additions & 7 deletions com.unity.cinemachine/Tests/Editor/BlendManagerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -57,21 +58,26 @@ [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);
}

[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);
Expand Down Expand Up @@ -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);
}
}
}
2 changes: 1 addition & 1 deletion com.unity.cinemachine/package.json
Original file line number Diff line number Diff line change
@@ -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": [
Expand Down

0 comments on commit 6c06201

Please sign in to comment.