Adding actor components to spawnable actor using LevelSequences in Unreal 5.5

It seems there’s been a change to how Spawnable bindings work for MovieSceneSequences between UE 5.4 and 5.5. I’m trying to decode the source code for now, but maybe someone knows and is willing to share knowledge - how does one add a component track to an actor track using C++?

I had to change how I add a spawnable actor too - before I could just spawn a transient actor to GEditor world, use Cast<UMovieSceneSequence>(LevelSequence)->CreateSpawnable(TransientActor), and remove the transient actor from the world. Now I have to use the ULevelSequence::MakeSpawnableTemplateFromInstance and pass the result to CreateSpawnable. It works for creating spawnables and not making a mess, but now I have a hard time adding component tracks. I try to add the components using

LevelSequence->GetMovieScene()->AddPossessable(SomeName, Component->GetClass());
FMovieScenePossessable * ComponentPossessable = Sequence->GetMovieScene()->FindPossessable(ComponentGuid);
LevelSequence->BindPossessableObject(ComponentGuid, *Component, Owner);
ComponentPossessable->SetParent(OwnerGuid, LevelSequence->GetMovieScene());

but well… in 5.4 I also used FMovieSceneSpawnable::AddChildPossessable in between to make it work but… FMovieSceneSpawnable do not work in 5.5.

1 Like

Had to use a modified version of MovieSceneHelpers::MakeSpawnableTemplateFromInstance that uses a different NewObject call, that is

UObject* NewInstance = NewObject<UObject>(InMovieScene, InSourceObject.GetClass(), InName, RF_NoFlags, &InSourceObject, true);

Otherwise the new actor had no components. Tested only on Blueprint Actors.

Oh thank you god !!!

I was fighting with the “““documentation”””, source code, and chatGPT to create a spawnable !!! I didn’t see ANYWHERE (even source code) the method MakeSpawnableTemplateFromInstance

I was just doing the CreateSpawnable alone (and passing it an object) but I got some weird things happening.
The object was really added as a spawnable, but when I looked at the object in the level sequence in the outliner, EVERY SINGLE COMPONENT inside the object was duplicated, and the duplicate were named TRASH_SceneComponent (for example).

I really hate unreal for this kind of things, because there is no clear documentation about any of this (at least that I know of).

I’m working on unreal 5.6 btw. And it worked for me.

Thank you for the help :man_bowing:t2::man_bowing:t2::man_bowing:t2::man_bowing:t2:

@mkloczko1

Do you still remember how you did this ?
Because I am finding myself with the same problem.

What I want is do add a component to an existing Spawnable actor that is inside the level sequence.
So, inside the level sequence, I added a spawnable MetaHuman. I can add animation to it without problem, because the animation is on the MetaHuman itself (the root/skeletal mesh component).

But now, I want to add the Face component of the MetaHuman to its parent in the level sequence, then add an animation to the Face.

Right now, I can add the Face and the animation, but there seem to be some kind of problem as the face animation is not playing, and the Face track is grayed out….

Here is what I am doing in blueprint to add the Metahuman, its animation, and the Face and animation I want.

Here is all my code :

#include "RuntimeSequenceManager.h"

#include "EngineUtils.h"
#include "LevelSequence.h"
#include "LevelSequenceActor.h"
#include "MovieScene.h"
#include "Animation/AnimSequence.h"
#include "Camera/CameraActor.h"
#include "Camera/CameraComponent.h"
#include "MoviePipelineExecutor.h"
#include "MoviePipelineInProcessExecutor.h"
#include "MoviePipelineQueueEngineSubsystem.h"
#include "Sections/MovieSceneCameraCutSection.h"
#include "Tracks/MovieScene3DTransformTrack.h"
#include "Tracks/MovieSceneCameraCutTrack.h"
#include "Tracks/MovieSceneSkeletalAnimationTrack.h"


// ===================== Private =====================

FGuid URuntimeSequenceManager::CreateBinding(ULevelSequence* Sequence, UObject* Object, bool bSpawnable)
{
    if (!Sequence || !Object)
    {
        UE_LOG(LogTemp, Error, TEXT("CreateBinding: Invalid Sequence or Object."));
        return FGuid();
    }

    UMovieSceneSequence* MovieSceneSequence = Cast<UMovieSceneSequence>(Sequence);
    if (!MovieSceneSequence)
    {
        UE_LOG(LogTemp, Error, TEXT("CreateBinding: Sequence is not a MovieSceneSequence."));
        return FGuid();
    }

    FGuid Guid;

    if (bSpawnable)
    {
        // Duplicate and clean the duplicate (to avoid modifying the actual object (because it's a pointer to the actual object)
        UObject* Copy = DuplicateObjectForSequencer(Sequence, Object);
        PrepareObjectForSequencer(Copy);
        
        // UObject* Template = Sequence->MakeSpawnableTemplateFromInstance(*Copy, *Copy->GetName());
        UObject* Template = MakeSpawnableTemplateFromInstance(*Copy, Sequence->MovieScene, *Copy->GetName());
        Guid = MovieSceneSequence->CreateSpawnable(Template);
        
        // UObject* Template = Sequence->MakeSpawnableTemplateFromInstance(*Object, *Object->GetName());
        // Guid = MovieSceneSequence->CreateSpawnable(Template);
        // Remove animation blueprint on TEMPLATE
        if (Guid.IsValid()) AddedBindings.Add(Guid, Template);
        UE_LOG(LogTemp, Log, TEXT("CreateBinding: Created SPAWNABLE %s with GUID %s."), *Template->GetName(), *Guid.ToString());
    }
    else
    {
        Guid = MovieSceneSequence->CreatePossessable(Object);
        if (Guid.IsValid()) AddedBindings.Add(Guid, Object);
        UE_LOG(LogTemp, Log, TEXT("CreateBinding: Created POSSESSABLE %s with GUID %s."), *Object->GetName(), *Guid.ToString());
    }

    return Guid;
}

FGuid URuntimeSequenceManager::AddCameraBinding(ULevelSequence* Sequence, bool bSpawnable)
{
    if (!Sequence)
    {
        UE_LOG(LogTemp, Error, TEXT("AddCameraBinding: Sequence is null."));
        return FGuid();
    }
    
    // Create the camera TEMPLATE inside the LevelSequence (for both spawnable and possessable)
    ACameraActor* TemplateCamera = NewObject<ACameraActor>(Sequence, ACameraActor::StaticClass(), NAME_None, RF_Transactional);
    if (!TemplateCamera)
    {
        UE_LOG(LogTemp, Error, TEXT("AddCameraBinding: Failed to create camera template."));
        return FGuid();
    }
    TemplateCamera->SetActorLabel(TEXT("RuntimeCamera"));

    const FGuid CameraGuid = CreateBinding(Sequence, TemplateCamera, bSpawnable);
    return CameraGuid;
}

void URuntimeSequenceManager::CopyCameraComponentSettings(const UCameraComponent* Source, const ACameraActor* DestinationActor)
{
    if (!Source || !DestinationActor)
    {
        UE_LOG(LogTemp, Error, TEXT("CopyCameraComponentSettings: Source or Destination is null."));
        return;
    }
    
    // Copy camera properties from source to destination
    if (UCameraComponent* Destination = DestinationActor->GetCameraComponent())
    {
        Destination->FieldOfView = Source->FieldOfView;
        Destination->AspectRatio = Source->AspectRatio;
        Destination->bConstrainAspectRatio = Source->bConstrainAspectRatio;
        Destination->ProjectionMode = Source->ProjectionMode;
        Destination->PostProcessSettings = Source->PostProcessSettings;
        Destination->PostProcessBlendWeight = Source->PostProcessBlendWeight;
        Destination->bConstrainAspectRatio = Source->bConstrainAspectRatio;
        Destination->bUsePawnControlRotation = Source->bUsePawnControlRotation;
    } 
    
    UE_LOG(LogTemp, Log, TEXT("CopyCameraComponentSettings: Copied camera settings from template."));
}

void URuntimeSequenceManager::CopyTransformToSpawnable(ULevelSequence* Sequence, FGuid Guid, const FTransform& TransformToApply, int KeyInterpolation)
{
    if (!Sequence || !Guid.IsValid())
        return;

    // IMPORTANT: Because we used "CreateSpawnable" before, the object was added to the "Possessables" Array (and not "Spawnables" !!!),
    // SO we use "FindPossessable". Otherwise, "FindSpawnable" will return null. (tested before, and indeed, it returns null.....)
    FMovieScenePossessable* Possessable = Sequence->MovieScene->FindPossessable(Guid);
    if (!Possessable)
    {
        UE_LOG(LogTemp, Error, TEXT("CopyTransformToSpawnable: Possessable/Spawnable not found."));
        return;
    }
    
    UObject* TemplateObject = nullptr;
    if (TWeakObjectPtr<UObject>* WeakObjPtr = AddedBindings.Find(Guid))
        TemplateObject = WeakObjPtr->Get();
    
    if (!TemplateObject)
    {
        UE_LOG(LogTemp, Error, TEXT("CopyTransformToSpawnable: Failed to get actor from AddedBindings."));
        return;
    }

    // Add transform, section and transform key frame
    UMovieScene3DTransformTrack* TransformTrack = AddTransformTrack(Sequence, TemplateObject);
    // todo: have some kind of variables that makes it so that I can choose the start and end frame here
    UMovieScene3DTransformSection* TransformSection = AddTransformSection(Sequence, TransformTrack, 0, 100, EMovieSceneBlendType::Absolute);
    AddTransformKeyFrame(Sequence, TemplateObject, TransformSection, 0, 0, TransformToApply, KeyInterpolation);

    UE_LOG(LogTemp, Log, TEXT("CopyTransformToSpawnable: Applied transform key to spawnable %s."), *Guid.ToString());
}

UObject* URuntimeSequenceManager::DuplicateObjectForSequencer(ULevelSequence* Sequence, UObject* OriginalObject)
{
    return StaticDuplicateObject(
        OriginalObject,
        Sequence, // Outer = LevelSequence to avoid GC
        NAME_None
    );
}

void URuntimeSequenceManager::PrepareObjectForSequencer(UObject* Object)
{
    AActor* ActorCopy = Cast<AActor>(Object);
    if (!ActorCopy)
    {
        UE_LOG(LogTemp, Error, TEXT("PrepareObjectForSequence: Object is not an actor."));
        return;
    }
    
    if (USkeletalMeshComponent* Sk = ActorCopy->FindComponentByClass<USkeletalMeshComponent>())
    {
        Sk->SetAnimationMode(EAnimationMode::AnimationCustomMode);
        Sk->AnimationData.AnimToPlay = nullptr; // to be safe
        Sk->AnimClass = nullptr; // to be safe
        /*
         * Because we remove the animation, and so we are removing the code that was attaching both hands to the wheel,
         * we should add a Control rig track to the character so that it is used at runtime in the level sequence.
         */
    }
}

UObject* URuntimeSequenceManager::MakeSpawnableTemplateFromInstance(UObject& InSourceObject, UMovieScene* InMovieScene, FName InName)
{
    // UObject* NewInstance = NewObject<UObject>(InMovieScene, InSourceObject.GetClass(), InName);
    UObject* NewInstance = NewObject<UObject>(InMovieScene, InSourceObject.GetClass(), InName, RF_NoFlags, &InSourceObject, true);

    UEngine::FCopyPropertiesForUnrelatedObjectsParams CopyParams;
    CopyParams.bNotifyObjectReplacement = false;
    CopyParams.bPreserveRootComponent = false;
    CopyParams.bPerformDuplication = true;
    UEngine::CopyPropertiesForUnrelatedObjects(&InSourceObject, NewInstance, CopyParams);

    AActor* Actor = CastChecked<AActor>(NewInstance);
    
    // Remove tags that may have gotten stuck on- for spawnables/replaceables these tags will be added after spawning
    static const FName SequencerActorTag(TEXT("SequencerActor"));
    static const FName SequencerPreviewActorTag(TEXT("SequencerPreviewActor"));
    Actor->Tags.Remove(SequencerActorTag);
    Actor->Tags.Remove(SequencerPreviewActorTag);

    if (Actor->GetAttachParentActor() != nullptr)
    {
        // We don't support spawnables and attachments right now
        // @todo: map to attach track?
        Actor->DetachFromActor(FDetachmentTransformRules(FAttachmentTransformRules(EAttachmentRule::KeepRelative, false), false));
    }

    // The spawnable source object was created with RF_Transient. The object generated from that needs its 
    // component flags cleared of RF_Transient so that the template object can be saved to the level sequence.
    for (UActorComponent* Component : Actor->GetComponents())
    {
        if (Component)
        {
            Component->ClearFlags(RF_Transient);
        }
    }

    return NewInstance;
}

void URuntimeSequenceManager::AddKeyFrameToDoubleChannel(UMovieSceneSection* Section, int ChannelIndex, int Frame, double Value, int KeyInterpolation)
{
    if (!Section)
    {
        UE_LOG(LogTemp, Error, TEXT("AddKeyFrameToDoubleChannel: Section not found."));
        return;
    }
    
    FMovieSceneDoubleChannel* Channel = Section->GetChannelProxy().GetChannel<FMovieSceneDoubleChannel>(ChannelIndex);
    if (!Channel)
    {
        UE_LOG(LogTemp, Error, TEXT("AddKeyFrameToDoubleChannel: Channel not found."));
        return;
    }
    
    // Calculate the tick value for the input frame numbers
    // For example, 100 ticks for one frame
    ULevelSequence* Sequence = Cast<ULevelSequence>(Section->GetOutermostObject());
    int FrameTickValue = Sequence->MovieScene->GetTickResolution().AsDecimal() / Sequence->MovieScene->GetDisplayRate().AsDecimal();
    FFrameNumber FrameNumber = FFrameNumber(Frame * FrameTickValue);
    
    // Add key to channel
    if (KeyInterpolation == 0)
        Channel->AddCubicKey(FrameNumber, Value);
    else if (KeyInterpolation == 1)
        Channel->AddLinearKey(FrameNumber, Value);
    else
        Channel->AddConstantKey(FrameNumber, Value);
}

void URuntimeSequenceManager::RemoveKeyFrameToDoubleChannel(UMovieSceneSection* Section, int ChannelIndex, int Frame)
{
    if (!Section)
    {
        UE_LOG(LogTemp, Error, TEXT("AddKeyFrameToDoubleChannel: Section not found."));
        return;
    }
    
    FMovieSceneDoubleChannel* Channel = Section->GetChannelProxy().GetChannel<FMovieSceneDoubleChannel>(ChannelIndex);
    if (!Channel)
    {
        UE_LOG(LogTemp, Error, TEXT("AddKeyFrameToDoubleChannel: Channel not found."));
        return;
    }
    
    // Calculate the tick value for the input frame numbers
    // For example, 100 ticks for one frame
    ULevelSequence* Sequence = Cast<ULevelSequence>(Section->GetOutermostObject());
    int FrameTickValue = Sequence->MovieScene->GetTickResolution().AsDecimal() / Sequence->MovieScene->GetDisplayRate().AsDecimal();
    FFrameNumber FrameNumber = FFrameNumber(Frame * FrameTickValue);
    
    // Find the key we want to remove
    TArray<FFrameNumber> UnusedKeyTimes;
    TArray<FKeyHandle> KeyHandles;
    Channel->GetKeys(TRange<FFrameNumber>(FrameNumber, FrameNumber), &UnusedKeyTimes, &KeyHandles);
    
    // Remove key
    Channel->DeleteKeys(KeyHandles);
}

FFrameNumber URuntimeSequenceManager::FindLastFrameInSequence(ULevelSequence* Sequence)
{
    // Get all the sections inside the level sequence
    TArray<UMovieSceneSection*> AllSections = Sequence->MovieScene->GetAllSections();
    FFrameNumber LastFrame = 0;
    
    for (UMovieSceneSection* Section : AllSections)
    {
        if (!Section) continue;
        
        // Get the start and end frame value
        TRange<FFrameNumber> SectionRange = Section->GetRange();
        
        if (SectionRange.HasUpperBound())
        {
            const FFrameNumber SectionEnd = SectionRange.GetUpperBoundValue();
            if (SectionEnd > LastFrame)
                LastFrame = SectionEnd;
        }
    }
    
    return LastFrame;
}

void URuntimeSequenceManager::SetPlaybackRangeToLastFrame(ULevelSequence* Sequence, FFrameNumber LastFrame)
{
    // Change the end of the level sequence to the last frame available from any section
    Sequence->MovieScene->SetPlaybackRange(TRange<FFrameNumber>(0, LastFrame));
}

void URuntimeSequenceManager::AddFinalStaticKeyFrames(ULevelSequence* Sequence, FFrameNumber LastFrame)
{
    if (!Sequence)
    {
        UE_LOG(LogTemp, Error, TEXT("AddFinalStaticKeyFrames: Sequence is null."));
        return;
    }
    
    if (LastFrame < 0)
    {
        UE_LOG(LogTemp, Error, TEXT("AddFinalStaticKeyFrames: Last frame is negative."));
        return;
    }
    
    if (StaticObjects.IsEmpty())
    {
        UE_LOG(LogTemp, Warning, TEXT("AddFinalStaticKeyFrames: Static objects list is empty."));
        return;
    }
    
    // Get the display rate to convert ticks in frames
    FFrameRate DisplayRate = Sequence->MovieScene->GetDisplayRate();
    FFrameRate TickResolution = Sequence->MovieScene->GetTickResolution();
    FFrameNumber LastFrameInFrames = FFrameRate::TransformTime(FFrameTime(LastFrame), TickResolution, DisplayRate).FloorToFrame();
    
    for (const TPair<TWeakObjectPtr<UObject>, FTransform>& Pair : StaticObjects)
    {
        TWeakObjectPtr<UObject> Object = Pair.Key;
        FTransform Transform = Pair.Value;

        // Get the transform track
        FGuid OutGuid;
        UMovieScene3DTransformTrack* Track = GetTransformTrackFromSequence(Sequence, Object.Get(), OutGuid);
        if (!Track)
        {
            UE_LOG(LogTemp, Warning, TEXT("AddFinalStaticKeyFrames: Transform track not found for object."));
            continue;
        }

        // Get the section (always index 0 for static)
        UMovieScene3DTransformSection* Section = GetTransformSectionFromSequence(Sequence, Object.Get(), Track, 0);
        if (!Section)
        {
            UE_LOG(LogTemp, Warning, TEXT("AddFinalStaticKeyFrames: Section not found for object."));
            continue;
        }
        
        // Extend the section until LastFrame
        if (Section->GetRange().GetUpperBoundValue() <= LastFrame)
            Section->SetRange(TRange<FFrameNumber>(Section->GetRange().GetLowerBoundValue(), LastFrame));

        // Add the last keyframe
        AddTransformKeyFrame(Sequence, Object.Get(), Section, 0, LastFrameInFrames.Value, Transform, 0);
        
        UE_LOG(LogTemp, Log, TEXT("AddFinalStaticKeyFrames: Static key added at last frame %d for object %s"), LastFrameInFrames.Value, *Object->GetName());
    }
    
    
    UE_LOG(LogTemp, Log, TEXT("AddFinalStaticKeyFrames: Finished adding final static key frames."));
}

void URuntimeSequenceManager::ExtendAllTracksToFrame(ULevelSequence* Sequence, FFrameNumber Frame)
{
    if (!Sequence)
    {
        UE_LOG(LogTemp, Error, TEXT("ExtendAllTracksToFrame: Sequence is null."));
        return;
    }

    for (const FMovieSceneBinding& Binding : Sequence->MovieScene->GetBindings())
    {
        for (UMovieSceneTrack* Track : Binding.GetTracks())
        {
            if (!Track) continue;

            for (UMovieSceneSection* Section : Track->GetAllSections())
            {
                if (!Section) continue;

                TRange<FFrameNumber> Range = Section->GetRange();

                FFrameNumber Lower = Range.HasLowerBound() ? Range.GetLowerBoundValue() : 0; // fallback
                FFrameNumber Upper = Range.HasUpperBound() ? Range.GetUpperBoundValue() : 0;

                if (!Range.HasUpperBound() || Upper < Frame)
                    Section->SetRange(TRange<FFrameNumber>(Lower, Frame));
            }
        }
    }

    // Don't forget the camera cut track, which is specific
    if (UMovieSceneCameraCutTrack* CutTrack = Cast<UMovieSceneCameraCutTrack>(Sequence->MovieScene->GetCameraCutTrack()))
    {
        for (UMovieSceneSection* Section : CutTrack->GetAllSections())
        {
            if (!Section) continue;
    
            TRange<FFrameNumber> Range = Section->GetRange();
            FFrameNumber Lower = Range.GetLowerBoundValue();
    
            if (Range.GetUpperBoundValue() < Frame)
                Section->SetRange(TRange<FFrameNumber>(Lower, Frame));
        }
    }
}



// ===================== Protected =====================

// ---------- Tracks ----------
UMovieSceneCameraCutTrack* URuntimeSequenceManager::GetCameraCutTrackFromSequence(ULevelSequence* Sequence)
{
    if (!Sequence)
    {
        UE_LOG(LogTemp, Error, TEXT("GetCameraCutTrackFromSequence: The Level Sequence is null."));
        return nullptr;
    }
    
    // Get the camera cut track
    UMovieSceneCameraCutTrack* CameraCutTrack = Cast<UMovieSceneCameraCutTrack>(Sequence->MovieScene->GetCameraCutTrack());
    return CameraCutTrack;
}

UMovieScene3DTransformTrack* URuntimeSequenceManager::GetTransformTrackFromSequence(ULevelSequence* Sequence, UObject* Object, FGuid& OutGuid)
{
    FGuid Guid = GetGuidFromObjectInSequence(Sequence, Object); // we already check if object and sequence are null inside this method
    if (!Guid.IsValid())
    {
        UE_LOG(LogTemp, Error, TEXT("GetTransformTrackFromSequence: Actor is not in the sequence."));
        OutGuid = Guid;
        return nullptr;
    }
    OutGuid = Guid;
    
    // Get the transform track
    UMovieScene3DTransformTrack* TransformTrack = Sequence->MovieScene->FindTrack<UMovieScene3DTransformTrack>(Guid);
    return TransformTrack;
}

UMovieSceneSkeletalAnimationTrack* URuntimeSequenceManager::GetOrAddAnimationTrackFromSequence(ULevelSequence* Sequence, UObject* Object, FGuid& OutGuid)
{
    FGuid Guid = GetGuidFromObjectInSequence(Sequence, Object); // we already check if object and sequence are null inside this method
    if (!Guid.IsValid())
    {
        UE_LOG(LogTemp, Error, TEXT("GetAnimationTrackFromSequence: Object is not in the sequence."));
        OutGuid = Guid;
        return nullptr;
    }
    OutGuid = Guid;
    
    // Get the animation track
    UMovieSceneSkeletalAnimationTrack* AnimTrack = Sequence->MovieScene->FindTrack<UMovieSceneSkeletalAnimationTrack>(Guid);
    if (AnimTrack == nullptr)
    {
        // Create the animation track if it doesn't exist
        AnimTrack = Sequence->MovieScene->AddTrack<UMovieSceneSkeletalAnimationTrack>(Guid);
        if (!AnimTrack)
        {
            UE_LOG(LogTemp, Error, TEXT("GetAnimationTrackFromSequence: Was not able to create track."));
            return nullptr;
        }
        
        UE_LOG(LogTemp, Log, TEXT("GetAnimationTrackFromSequence: Created track for object %s."), *Object->GetName());
    }
    
    return AnimTrack;
}

UMovieScene3DTransformTrack* URuntimeSequenceManager::AddTransformTrack(ULevelSequence* Sequence, UObject* Object)
{
    FGuid Guid = FGuid();
    UMovieScene3DTransformTrack* TransformTrack = GetTransformTrackFromSequence(Sequence, Object, Guid);
    if (TransformTrack == nullptr)
    {
        // Create the transform track if it doesn't exist
        TransformTrack = Sequence->MovieScene->AddTrack<UMovieScene3DTransformTrack>(Guid);
        if (!TransformTrack)
        {
            UE_LOG(LogTemp, Error, TEXT("AddTransformKey: Was not able to create track."));
            return nullptr;
        }
        
        UE_LOG(LogTemp, Log, TEXT("AddTransformKey: Created track for object %s."), *Object->GetName());
    }
    
    return TransformTrack;
}

void URuntimeSequenceManager::AddCameraCutTrack(ULevelSequence* Sequence, FGuid CamGuid, int StartFrame, int EndFrame)
{
    // ------ Add the Camera cut track and the section for this camera ------
    UMovieSceneCameraCutTrack* CameraCutTrack = GetCameraCutTrackFromSequence(Sequence); // we already check here if the sequence is valid
    if (CameraCutTrack == nullptr)
    {
        // Add the camera cut track if it doesn't exist
        CameraCutTrack = Cast<UMovieSceneCameraCutTrack>(Sequence->MovieScene->AddCameraCutTrack(UMovieSceneCameraCutTrack::StaticClass()));
    }
    
    // Calculate the tick value for the input frame numbers
    // For example, 100 ticks for one frame
    int FrameTickValue = Sequence->MovieScene->GetTickResolution().AsDecimal() / Sequence->MovieScene->GetDisplayRate().AsDecimal();
    
    UMovieSceneCameraCutSection* Section = CameraCutTrack->AddNewCameraCut(UE::MovieScene::FRelativeObjectBindingID(CamGuid), FFrameNumber(StartFrame * FrameTickValue));
    if (!Section)
    {
        UE_LOG(LogTemp, Error, TEXT("AddCameraToSequence: Was not able to create section."));
    }
    
    if (EndFrame > StartFrame)
    {
        Section->SetEndFrame(FFrameNumber(EndFrame * FrameTickValue));
    }
    
    UE_LOG(LogTemp, Log, TEXT("AddCameraCutTrack: Added camera cut track for camera %s."), *CamGuid.ToString());
}

// ---------- Sections ----------
UMovieScene3DTransformSection* URuntimeSequenceManager::GetTransformSectionFromSequence(ULevelSequence* Sequence, UObject* Object, UMovieScene3DTransformTrack* TransformTrack, int SectionIndex)
{
    if (!TransformTrack)
    {
        UE_LOG(LogTemp, Error, TEXT("GetTransformSectionFromSequence: Transform track is null."));
        return nullptr;
    }
    
    // Get all sections
    TArray<UMovieSceneSection*> AllSections = TransformTrack->GetAllSections();
    
    // Make sure the index is valid
    if (SectionIndex < 0 || SectionIndex >= AllSections.Num())
    {
        UE_LOG(LogTemp, Error, TEXT("GetTransformSectionFromSequence: Section index out of bounds."));
        return nullptr;
    }
    
    return Cast<UMovieScene3DTransformSection>(AllSections[SectionIndex]);
}

UMovieScene3DTransformSection* URuntimeSequenceManager::AddTransformSection(ULevelSequence* Sequence, UMovieScene3DTransformTrack* Track, int StartFrame, int EndFrame, EMovieSceneBlendType BlendType)
{
    // Create the section
    UMovieScene3DTransformSection* TransformSection = Cast<UMovieScene3DTransformSection>(Track->CreateNewSection());
    if (!TransformSection)
    {
        UE_LOG(LogTemp, Error, TEXT("AddTransformKey: Was not able to create section."));
        return nullptr;
    }
    
    // Calculate the tick value for the input frame numbers
    // For example, 100 ticks for one frame
    int FrameTickValue = Sequence->MovieScene->GetTickResolution().AsDecimal() / Sequence->MovieScene->GetDisplayRate().AsDecimal();
    
    // Set the frame range of the section
    TransformSection->SetRange((TRange<FFrameNumber>(FFrameNumber(StartFrame * FrameTickValue), FFrameNumber(EndFrame * FrameTickValue))));
    
    // Set the blend type
    TransformSection->SetBlendType(BlendType);
    
    int RowIndex = -1;
    for (UMovieSceneSection* ExistingSection : Track->GetAllSections())
    {
        RowIndex = FMath::Max(RowIndex, ExistingSection->GetRowIndex());
    }
    TransformSection->SetRowIndex(RowIndex + 1);
    
    // Add Section to track
    Track->AddSection(*TransformSection);
    
    return TransformSection;
}



// ===================== Public =====================

// ---------- Setup ----------
ALevelSequenceActor* URuntimeSequenceManager::GetOrSpawnSequenceActor(UWorld* World, ULevelSequence* Sequence)
{
    if (!World || !Sequence) return nullptr;

    // Search for a LevelSequenceActor existing which plays this sequence
    for (TActorIterator<ALevelSequenceActor> It(World); It; ++It)
    {
        if (It->GetSequence() == Sequence)
            return *It;
    }

    // If none are found
    FActorSpawnParameters SpawnParams;
    SpawnParams.Name = FName(TEXT("RuntimeSequenceActor"));
    ALevelSequenceActor* NewActor = World->SpawnActor<ALevelSequenceActor>(ALevelSequenceActor::StaticClass(), FTransform::Identity, SpawnParams);
    if (NewActor)
    {
        NewActor->SetSequence(Sequence);
        NewActor->InitializePlayer(); // Assure que SequencePlayer est créé
    }

    return NewActor;
}

FGuid URuntimeSequenceManager::GetGuidFromObjectInSequence(ULevelSequence* Sequence, UObject* Object)
{
    if (!Object)
    {
        UE_LOG(LogTemp, Error, TEXT("GetObjectGuidFromSequence: The Object you want to get is null."));
        return FGuid();
    }
    if (!Sequence)
    {
        UE_LOG(LogTemp, Error, TEXT("GetObjectGuidFromSequence: The Level Sequence is null."));
        return FGuid();
    }
    
    // First: fast path using our local map (recommended for runtime usage)
    for (const auto& Pair : AddedBindings)
    {
        if (Pair.Value.IsValid() && Pair.Value.Get() == Object)
        {
            
            UE_LOG(LogTemp, Log, TEXT("GetObjectGuidFromSequence: Object found in local mapping!"));
            return Pair.Key;
        }
    }

    // If we don't own the mapping (Object might have been added by other code), we can't reliably resolve it
    // without engine internals (SharedPlaybackState / Linker). So we return the invalid and let the caller add it.
    UE_LOG(LogTemp, Warning, TEXT("GetObjectGuidFromSequence: Object not found in local mapping."));
    return FGuid();
}

void URuntimeSequenceManager::ClearSequence(ULevelSequence *Sequence)
{
    if (!Sequence)
    {
        UE_LOG(LogTemp, Error, TEXT("ClearSequence: Sequence is null."));
        return;
    }

    if (AddedBindings.Num() == 0)
    {
        UE_LOG(LogTemp, Log, TEXT("ClearSequence: No stored bindings to clear."));
        return;
    }

    UMovieScene* MovieScene = Sequence->GetMovieScene();
    if (!MovieScene) return;
    
    // ----- Clear all bindings created by this runtime builder -----
    TArray<FGuid> Keys;
    AddedBindings.GenerateKeyArray(Keys);
    
    // Iterate over all stored bindings and remove them
    for (const FGuid& Guid : Keys)
    {
        Sequence->UnbindPossessableObjects(Guid); // Unbind any objects that were bound at this guid
        MovieScene->RemovePossessable(Guid); // Remove the possessable from the movie scene (safe in runtime)
    }
    
    AddedBindings.Empty(); // Clear the map
    
    // ----- Clear camera cuts -----
    UMovieSceneCameraCutTrack* CameraCutTrack = GetCameraCutTrackFromSequence(Sequence);
    if (CameraCutTrack && CameraCutTrack->GetAllSections().Num() > 0)
    {
        CameraCutTrack->RemoveAllAnimationData();
        Sequence->MovieScene->RemoveCameraCutTrack();
    }

    UE_LOG(LogTemp, Log, TEXT("ClearSequence: Sequence cleared and all stored bindings removed."));
}

void URuntimeSequenceManager::RenderSequence(ULevelSequence* Sequence, const FString& PresetPath)
{
    if (bIsRendering)
    {
        UE_LOG(LogTemp, Error, TEXT("RenderSequence: Already rendering a sequence."));
        return;
    }
    
    UMoviePipelineQueueEngineSubsystem* Subsystem = GEngine->GetEngineSubsystem<UMoviePipelineQueueEngineSubsystem>();
    UMoviePipelineQueue* Queue = Subsystem->GetQueue();
    Queue->DeleteAllJobs();
    
    // New job
    UMoviePipelineExecutorJob* Job = Queue->AllocateNewJob(UMoviePipelineExecutorJob::StaticClass());
    Job->Sequence = FSoftObjectPath(Sequence);
    Job->Map = FSoftObjectPath(GetWorld());
    Job->JobName = TEXT("RuntimeRender");
    
    if (!PresetPath.IsEmpty())
    {
        UMoviePipelinePrimaryConfig* Preset = LoadObject<UMoviePipelinePrimaryConfig>(nullptr, *PresetPath);
        if (Preset)
            Job->SetConfiguration(Preset);
        else
            UE_LOG(LogTemp, Error, TEXT("RenderSequence: Could not load movie pipeline configuration at path %s."), *PresetPath);
    }
    
    // Create an executor that will render the queue in the current level !!
    UMoviePipelineInProcessExecutor* Executor = NewObject<UMoviePipelineInProcessExecutor>();
    Executor->bUseCurrentLevel = true;
    
    // Add a callbakcs to notify us when the executor is finished
    Executor->OnExecutorFinished().AddLambda([this](UMoviePipelineExecutorBase*, bool)
    {
        UE_LOG(LogTemp, Log, TEXT("RenderSequence: Render finished."));
        bIsRendering = false;
    });
    
    // Execute
    bIsRendering = true;
    Subsystem->RenderQueueWithExecutorInstance(Executor);
    
    UE_LOG(LogTemp, Log, TEXT("RenderSequence: Render started."));
}

// ---------- Add Objects ----------
FGuid URuntimeSequenceManager::AddCameraActorToSequence(ULevelSequence *Sequence, ACameraActor *SourceCameraActor, int StartFrame, int EndFrame, bool bSpawnable, bool bStaticTransform)
{
    // todo:
    // check if a camera is not already in the level sequence
    // we could add multiple camera, but it is not usefull right now
    
    FGuid CamGuid = AddCameraBinding(Sequence, bSpawnable);
    if (!CamGuid.IsValid())
    {
        UE_LOG(LogTemp, Error, TEXT("AddCameraActorToSequence: Failed to add camera spawnable to level sequence."));
        return FGuid();
    }
    
    // ------ Copy the camera components settings to the spawnable camera actor ------
    // Get the actor associated with the Guid
    ACameraActor* TemplateCamera = Cast<ACameraActor>(AddedBindings[CamGuid].Get());
    if (!TemplateCamera)
    {
        UE_LOG(LogTemp, Error, TEXT("AddCameraActorToSequence: Template camera not found in AddedBindings."));
        return CamGuid;
    }
    
    // Copy settings
    CopyCameraComponentSettings(SourceCameraActor->GetCameraComponent(), TemplateCamera);
    
    // Add camera cut
    AddCameraCutTrack(Sequence, CamGuid, StartFrame, EndFrame);
    
    // Add transform track and key
    FTransform SourceTransform = SourceCameraActor->GetTransform();
    UMovieScene3DTransformTrack* TransformTrack = AddTransformTrack(Sequence, TemplateCamera);
    UMovieScene3DTransformSection* TransformSection = AddTransformSection(Sequence, TransformTrack, StartFrame, EndFrame, EMovieSceneBlendType::Absolute);
    AddTransformKeyFrame(Sequence, TemplateCamera, TransformSection, 0, StartFrame, SourceTransform, 0);
    UE_LOG(LogTemp, Log, TEXT("AddCameraActorToSequence: Added camera component to sequence."));
    
    if (bStaticTransform)
        StaticObjects.Add(TemplateCamera, SourceTransform); // Add to the map to add the last keyframe later (in SetSequenceEndToLastKey)
    
    return CamGuid;
}

FGuid URuntimeSequenceManager::AddCameraComponentToSequence(ULevelSequence* Sequence, UCameraComponent* SourceCameraComponent, int StartFrame, int EndFrame, bool bSpawnable, bool bStaticTransform)
{
    // todo:
    // check if a camera is not already in the level sequence
    // we could add multiple camera, but it is not usefull right now
    
    FGuid CamGuid = AddCameraBinding(Sequence, bSpawnable);
    if (!CamGuid.IsValid())
    {
        UE_LOG(LogTemp, Error, TEXT("AddCameraComponentToSequence: Failed to add camera spawnable to level sequence."));
        return FGuid();
    }
    
    // ------ Copy the camera components settings to the spawnable camera actor ------
    // Get the actor associated with the Guid
    ACameraActor* TemplateCamera = Cast<ACameraActor>(AddedBindings[CamGuid].Get());
    if (!TemplateCamera)
    {
        UE_LOG(LogTemp, Error, TEXT("AddCameraComponentToSequence: Failed to get camera actor from spawnable."));
        return FGuid();
    }
    
    // Copy settings
    CopyCameraComponentSettings(SourceCameraComponent, TemplateCamera);
    
    // Add camera cut
    AddCameraCutTrack(Sequence, CamGuid, StartFrame, EndFrame);
    
    // Add transform track and key
    FTransform SourceTransform = SourceCameraComponent->GetComponentTransform();
    UMovieScene3DTransformTrack* TransformTrack = AddTransformTrack(Sequence, TemplateCamera);
    UMovieScene3DTransformSection* TransformSection = AddTransformSection(Sequence, TransformTrack, StartFrame, EndFrame, EMovieSceneBlendType::Absolute);
    AddTransformKeyFrame(Sequence, TemplateCamera, TransformSection, 0, StartFrame, SourceTransform, 0);
    UE_LOG(LogTemp, Log, TEXT("AddCameraComponentToSequence: Added camera component to sequence."));
    
    if (bStaticTransform)
        StaticObjects.Add(TemplateCamera, SourceTransform); // Add to the map to add the last keyframe later (in SetSequenceEndToLastKey)
    
    return CamGuid;
}

FGuid URuntimeSequenceManager::AddObjectToSequence(ULevelSequence *Sequence, UObject* Object, UObject*& OutTemplateObject, bool bSpawnable = true, bool bSyncTransform = true, bool bStaticTransform = false, FTransform Transform = FTransform::Identity)
{
    OutTemplateObject = nullptr;
    
    FGuid Guid = GetGuidFromObjectInSequence(Sequence, Object); // we already check if object and sequence are null inside this method
    if (Guid.IsValid())
    {
        UE_LOG(LogTemp, Log, TEXT("AddObjectToSequence: Object %s already exists!"), *Object->GetName());
        if (TWeakObjectPtr<UObject>* WeakObjPtr = AddedBindings.Find(Guid))
            OutTemplateObject = WeakObjPtr->Get();
        return Guid;
    }
    
    FGuid BindingGuid = CreateBinding(Sequence, Object, bSpawnable);
    
    if (bSyncTransform)
        CopyTransformToSpawnable(Sequence, BindingGuid, Transform, 0);
    
    if (TWeakObjectPtr<UObject>* WeakObjPtr = AddedBindings.Find(BindingGuid))
        OutTemplateObject = WeakObjPtr->Get();
    
    if (bStaticTransform)
    {
        if (OutTemplateObject)
            StaticObjects.Add(OutTemplateObject, Transform); // Add to the map to add the last keyframe later (in SetSequenceEndToLastKey)
        else
        {
            UE_LOG(LogTemp, Error, TEXT("AddObjectToSequence: Failed to get template object from binding guid."));
        }
    }
    
    return BindingGuid;
}

FGuid URuntimeSequenceManager::AddChildBindingToSequence(ULevelSequence* Sequence, const FGuid& ParentGuid, UObject* ChildObject, UObject*& OutChildTemplateObject)
{
    OutChildTemplateObject = nullptr;
    
    if (!Sequence || !ChildObject || !ParentGuid.IsValid())
    {
        UE_LOG(LogTemp, Error, TEXT("AddChildBindingToSequence: Invalid input"));
        return FGuid();
    }
    
    // Create a possessable, then it will be parented to the ParentGuid object
    FGuid ChildGuid = Sequence->MovieScene->AddPossessable(ChildObject->GetName(), ChildObject->GetClass());
    if (!ChildGuid.IsValid())
    {
        UE_LOG(LogTemp, Error, TEXT("AddChildBindingToSequence: Failed to add child binding to sequence."));
        return FGuid();
    }
    
    Sequence->BindPossessableObject(ChildGuid, *ChildObject, ChildObject->GetWorld());
    FMovieScenePossessable* Possessable = Sequence->MovieScene->FindPossessable(ChildGuid);
    if (Possessable)
    {
        Possessable->SetParent(ParentGuid, Sequence->MovieScene);
        // Add the child object to the map to later be able to get it with GetGuidFromObjectInSequence()
        AddedBindings.Add(ChildGuid, ChildObject);
    }
    else
    {
        UE_LOG(LogTemp, Error, TEXT("AddChildBindingToSequence: Failed to find possessable."));
        return FGuid();
    }
    
    // Get the child template
    OutChildTemplateObject = ChildObject;
    
    UE_LOG(LogTemp, Log, TEXT("AddChildBindingToSequence: Child %s added under parent GUID %s"), *ChildObject->GetName(), *ParentGuid.ToString());
    return ChildGuid;
}

void URuntimeSequenceManager::AddAnimationToSequence(ULevelSequence *Sequence, UObject* Object, UAnimSequence *Anim, int StartFrame, int EndFrame)
{
    if (!Anim)
    {
        UE_LOG(LogTemp, Error, TEXT("AddAnimationToSequence: Animation is not valid."));
        return;
    }
    
    FGuid Guid = FGuid();
    UMovieSceneSkeletalAnimationTrack* AnimTrack = GetOrAddAnimationTrackFromSequence(Sequence, Object, Guid);
    
    // Calculate the tick value for the input frame numbers
    // For example, 100 ticks for one frame
    int FrameTickValue = Sequence->MovieScene->GetTickResolution().AsDecimal() / Sequence->MovieScene->GetDisplayRate().AsDecimal();
    
    // Add animation Track
    UMovieSceneSection* Section = AnimTrack->AddNewAnimation(FFrameNumber(StartFrame * FrameTickValue), Anim);
    if (!Section)
    {
        UE_LOG(LogTemp, Error, TEXT("AddAnimationToSequence: Was not able to create section."));
    }
    
    if (EndFrame > StartFrame)
    {
        Section->SetEndFrame(FFrameNumber(EndFrame * FrameTickValue));
    }
}

// ---------- Keyframing ----------
void URuntimeSequenceManager::AddTransformKeyFrame(ULevelSequence* Sequence, UObject* Object, UMovieScene3DTransformSection* Section, int SectionIndex, int Frame, FTransform Transform, int KeyInterpolation)
{
    if (!Section)
    {
        UE_LOG(LogTemp, Error, TEXT("AddTransformKey: Section is null."));
        return;
    }
    
    RemoveTransformKeyFrame(Section, Frame);
    
    // Location
    AddKeyFrameToDoubleChannel(Section, 0, Frame, Transform.GetLocation().X, KeyInterpolation);
    AddKeyFrameToDoubleChannel(Section, 1, Frame, Transform.GetLocation().Y, KeyInterpolation);
    AddKeyFrameToDoubleChannel(Section, 2, Frame, Transform.GetLocation().Z, KeyInterpolation);
    
    // Rotation
    AddKeyFrameToDoubleChannel(Section, 3, Frame, Transform.Rotator().Roll, KeyInterpolation);
    AddKeyFrameToDoubleChannel(Section, 4, Frame, Transform.Rotator().Pitch, KeyInterpolation);
    AddKeyFrameToDoubleChannel(Section, 5, Frame, Transform.Rotator().Yaw, KeyInterpolation);
    
    // Location
    AddKeyFrameToDoubleChannel(Section, 6, Frame, Transform.GetScale3D().X, KeyInterpolation);
    AddKeyFrameToDoubleChannel(Section, 7, Frame, Transform.GetScale3D().Y, KeyInterpolation);
    AddKeyFrameToDoubleChannel(Section, 8, Frame, Transform.GetScale3D().Z, KeyInterpolation);
    
    UE_LOG(LogTemp, Log, TEXT("AddTransformKey: Added key frame to section %d."), SectionIndex);
}

void URuntimeSequenceManager::RemoveTransformKeyFrame(UMovieScene3DTransformSection* Section, int Frame)
{
    if (!Section)
    {
        UE_LOG(LogTemp, Error, TEXT("AddTransformKey: Section not found."));
        return;
    }
    
    // Location
    RemoveKeyFrameToDoubleChannel(Section, 0, Frame);
    RemoveKeyFrameToDoubleChannel(Section, 1, Frame);
    RemoveKeyFrameToDoubleChannel(Section, 2, Frame);
    
    // Rotation
    RemoveKeyFrameToDoubleChannel(Section, 3, Frame);
    RemoveKeyFrameToDoubleChannel(Section, 4, Frame);
    RemoveKeyFrameToDoubleChannel(Section, 5, Frame);
    
    // Location
    RemoveKeyFrameToDoubleChannel(Section, 6, Frame);
    RemoveKeyFrameToDoubleChannel(Section, 7, Frame);
    RemoveKeyFrameToDoubleChannel(Section, 8, Frame);
    
    UE_LOG(LogTemp, Log, TEXT("AddTransformKey: Removed key frame from section %d."), Section->GetRowIndex());
}

// ---------- Finalize ----------
void URuntimeSequenceManager::FinalizeSequence(ULevelSequence* Sequence)
{
    FFrameNumber LastFrame = FindLastFrameInSequence(Sequence);
    SetPlaybackRangeToLastFrame(Sequence, LastFrame);
    AddFinalStaticKeyFrames(Sequence, LastFrame);
    ExtendAllTracksToFrame(Sequence, LastFrame);
}


void URuntimeSequenceManager::DebugTest(ULevelSequence* Sequence)
{
    FFrameNumber LastFrame = FindLastFrameInSequence(Sequence);
    ExtendAllTracksToFrame(Sequence, LastFrame);
}

If you know what’s happening, please let me know.