Playback of multiple camera-controlling Level Sequences & controlling the Player View Target

Hello all,

I’m working on some C++ code for a project which requires multiple Level Sequences in different parts of the same world.

I’m looking for high-level feedback/alternatives to the approach below. If anyone has any suggestions, or potential solutions that do not involve modifying Engine source code if possible.

Another options might be to implement a custom player controller.

The idea is that we play a given Level Sequence with a Camera Cut Track, but at any point, based on inputs, we may need to transition the master camera (Player View Target) away to another part of the world that has its own Level Sequence and smoothly play that back and blend into its current camera state.

Most of this is working at the time of this writing, but I’ve hit a bit of a wall and would appreciate any thoughts/insights.

Currently my code controls and plays back multiple Level Sequences – I can sample the cameras of each Level Sequence and then use that to override the Player’s View Target (SetViewTarget() / SetViewTargetWithBlend()), and control my own camera transitions as needed since I’m always in control of the player camera/View Target.

However, as you might expect, each Level Sequence with a Camera Cut Track wants to control the Player View Target.

ULevelSequencePlayer::UpdateCameraCut() is the meat of this logic. It conditionally sets the Player View Target, unless, for example, there is a pending camera blend/transition. I’m not using any blends between cameras, so this means that every frame my code (all defined within an Actor, which spawns its own ACameraActor on init to act as the master camera state) sees that the player’s current View Target isn’t our custom camera (since UpdateCameraCut() is also setting it), so it calls SetViewTarget() every frame and there is a trashing of camera cut logic due to both systems trying to control the Player’s View Target.

OK, well ULevelSequencePlayer::UpdateCameraCut() earlies-out in a few conditions, most notably:

	if (!CanUpdateCameraCut())
	{
		return;
	}

CanUpdateCameraCut() simply checks the playback settings for bDisableCameraCuts. OK, but setting that (as you might expect) breaks built-in camera functionality since NotifyCameraCut() no longer gets called, breaking things like camera-to-actor look-at logic.

A bit of a deeper dive into a potential solution:

If we look at ULevelSequencePlayer::UpdateCameraCut(), we can see that it takes an argument called CameraObject, and from I can see here, this could allow a caller to pass in a custom camera to be animated. All callsites I see pass nullptr for this argument, so presumably I’d need to override something somewhere, but even so, I’m not sure it helps, since the logic still wants to set the Player View Target in all cases that I care about.

Perhaps an approach might be to subclass ULevelSequencePlayer and override UpdateCameraCut() (which is overridable) if I can, or (please no) modify Engine code.

The full 5.1 source for this function for reference below. Any pro tips would be greatly appreciated, and thanks for reading!

void ULevelSequencePlayer::UpdateCameraCut(UObject* CameraObject, const EMovieSceneCameraCutParams& CameraCutParams)
{
	UCameraComponent* CameraComponent = MovieSceneHelpers::CameraComponentFromRuntimeObject(CameraObject);
	if (CameraComponent && CameraComponent->GetOwner() != CameraObject)
	{
		CameraObject = CameraComponent->GetOwner();
	}

	CachedCameraComponent = CameraComponent;
	
	if (World == nullptr || World->GetGameInstance() == nullptr)
	{
		return;
	}

	// skip missing player controller
	APlayerController* PC = World->GetGameInstance()->GetFirstLocalPlayerController();

	if (PC == nullptr)
	{
		return;
	}

	// skip same view target
	AActor* ViewTarget = PC->GetViewTarget();

	if (!CanUpdateCameraCut())
	{
		return;
	}

	if (CameraObject == ViewTarget)
	{
		if (CameraCutParams.bJumpCut)
		{
			if (PC->PlayerCameraManager)
			{
				PC->PlayerCameraManager->SetGameCameraCutThisFrame();
			}

			if (CameraComponent)
			{
				CameraComponent->NotifyCameraCut();
			}

			if (UMovieSceneMotionVectorSimulationSystem* MotionVectorSim = RootTemplateInstance.GetEntitySystemLinker()->FindSystem<UMovieSceneMotionVectorSimulationSystem>())
			{
				MotionVectorSim->SimulateAllTransforms();
			}
		}
		return;
	}

	// skip unlocking if the current view target differs
	AActor* UnlockIfCameraActor = Cast<AActor>(CameraCutParams.UnlockIfCameraObject);

	// if unlockIfCameraActor is valid, release lock if currently locked to object
	if (CameraObject == nullptr && UnlockIfCameraActor != nullptr && UnlockIfCameraActor != ViewTarget)
	{
		return;
	}

	// override the player controller's view target
	AActor* CameraActor = Cast<AActor>(CameraObject);
	ULocalPlayer* LocalPlayer = PC->GetLocalPlayer();

	// if the camera object is null, use the last view target so that it is restored to the state before the sequence takes control
	bool bRestoreAspectRatioConstraint = false;
	if (CameraActor == nullptr)
	{
		CameraActor = LastViewTarget.Get();
		bRestoreAspectRatioConstraint = true;

		// Skip if the last view target is the same as the current view target so that there's no additional camera cut
		if (CameraActor == ViewTarget)
		{
			if (LocalPlayer && LastAspectRatioAxisConstraint.IsSet())
			{
				LocalPlayer->AspectRatioAxisConstraint = LastAspectRatioAxisConstraint.GetValue();
			}
			return;
		}
	}

	// Save the last view target/aspect ratio constraint/etc. so that it can all be restored when the camera object is null.
	if (!LastViewTarget.IsValid())
	{
		LastViewTarget = ViewTarget;
	}
	if (!LastAspectRatioAxisConstraint.IsSet())
	{
		if (LocalPlayer != nullptr)
		{
			LastAspectRatioAxisConstraint = LocalPlayer->AspectRatioAxisConstraint;
		}
	}

	bool bDoSetViewTarget = true;
	FViewTargetTransitionParams TransitionParams;
	if (CameraCutParams.BlendType.IsSet())
	{
		UE_LOG(LogLevelSequence, Log, TEXT("Blending into new camera cut: '%s' -> '%s' (blend time: %f)"),
			(ViewTarget ? *ViewTarget->GetName() : TEXT("None")),
			(CameraObject ? *CameraObject->GetName() : TEXT("None")),
			TransitionParams.BlendTime);

		// Convert known easing functions to their corresponding view target blend parameters.
		TTuple<EViewTargetBlendFunction, float> BlendFunctionAndExp = BuiltInEasingTypeToBlendFunction(CameraCutParams.BlendType.GetValue());
		TransitionParams.BlendTime = CameraCutParams.BlendTime;
		TransitionParams.bLockOutgoing = CameraCutParams.bLockPreviousCamera;
		TransitionParams.BlendFunction = BlendFunctionAndExp.Get<0>();
		TransitionParams.BlendExp = BlendFunctionAndExp.Get<1>();

		// Calling SetViewTarget on a camera that we are currently transitioning to will 
		// result in that transition being aborted, and the view target being set immediately.
		// We want to avoid that, so let's leave the transition running if it's the case.
		if (PC->PlayerCameraManager != nullptr)
		{
			const AActor* CurViewTarget = PC->PlayerCameraManager->ViewTarget.Target;
			const AActor* PendingViewTarget = PC->PlayerCameraManager->PendingViewTarget.Target;
			if (CameraActor != nullptr && PendingViewTarget == CameraActor)
			{
				UE_LOG(LogLevelSequence, Log, TEXT("Camera transition aborted, we are already blending towards the intended camera"));
				bDoSetViewTarget = false;
			}
		}
	}
	else
	{
		UE_LOG(LogLevelSequence, Log, TEXT("Starting new camera cut: '%s'"),
			(CameraObject ? *CameraObject->GetName() : TEXT("None")));
	}
	if (bDoSetViewTarget)
	{
		PC->SetViewTarget(CameraActor, TransitionParams);
	}

	// Set or restore the aspect ratio constraint if we were overriding it for this sequence.
	if (LocalPlayer != nullptr && CameraSettings.bOverrideAspectRatioAxisConstraint)
	{
		if (bRestoreAspectRatioConstraint)
		{
			check(LastAspectRatioAxisConstraint.IsSet());
			if (LastAspectRatioAxisConstraint.IsSet())
			{
				LocalPlayer->AspectRatioAxisConstraint = LastAspectRatioAxisConstraint.GetValue();
			}
		}
		else
		{
			LocalPlayer->AspectRatioAxisConstraint = CameraSettings.AspectRatioAxisConstraint;
		}
	}

	// we want to notify of cuts on hard cuts and time jumps, but not on blend cuts
	const bool bIsStraightCut = !CameraCutParams.BlendType.IsSet() || CameraCutParams.bJumpCut;

	if (CameraComponent && bIsStraightCut)
	{
		CameraComponent->NotifyCameraCut();
	}

	if (PC->PlayerCameraManager)
	{
		PC->PlayerCameraManager->bClientSimulatingViewTarget = (CameraActor != nullptr);

		if (bIsStraightCut)
		{
			PC->PlayerCameraManager->SetGameCameraCutThisFrame();
		}
	}

	if (bIsStraightCut)
	{
		if (UMovieSceneMotionVectorSimulationSystem* MotionVectorSim = RootTemplateInstance.GetEntitySystemLinker()->FindSystem<UMovieSceneMotionVectorSimulationSystem>())
		{
			MotionVectorSim->SimulateAllTransforms();
		}

		if (OnCameraCut.IsBound())
		{
			OnCameraCut.Broadcast(CameraComponent);
		}
	}
}

Anyone? :] Please let me know if I can provide any clarification. I could really use some feedback/help here. Thanks.