Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CMCL-0000: CameraDeactivated events were not sent when a blend interrupted another #1028

Merged
merged 6 commits into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
}
}
4 changes: 2 additions & 2 deletions 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 Expand Up @@ -30,7 +30,7 @@
],
"category": "cinematography",
"dependencies": {
"com.unity.splines": "2.7.1"
"com.unity.splines": "2.0.0"
},
"samples": [
{
Expand Down
Loading