BlueRoseNote/03-UnrealEngine/Sequence/Sequence Runtime Binding.md

18 KiB
Raw Blame History

title, date, excerpt, tags, rating
title date excerpt tags rating
Sequence Runtime Binding 2024-05-16 17:55:58

前言

MovieSceneTextTrack 引擎插件可以作为参考。 UMovieSceneCVarTrackInstance

参考:

  1. sequencer mute tracks at runtime from c
  2. 其他参考
    1. [Actor Rebinding in Blueprints with Sequencer(官方文档)]https://docs.unrealengine.com/4.27/en-US/AnimatingObjects/Sequencer/HowTo/AnimateDynamicObjects/
    2. Level Sequence actor rebinding in C++
  3. Sequence ECS资料
    1. 大规模内容的性能保障虚幻引擎4.26中的Sequencer(官方文档)
    2. UE4.26(5.0)后的Sequence系统
    3. UE5 Sequence 浅-浅-析
  4. ECS资料
    1. [一文看懂ECS架构]https://zhuanlan.zhihu.com/p/618971664

可行思路:

  1. 给导播创建一个快捷键Hotkey之后在触发根据Tag切换对应角色动画蓝图中的逻辑。
  2. 使用Sequence的EventTrack设置若干事件。
  3. 模改UMovieSceneSkeletalAnimationTrack实现一个可以自动根据Tag捕获对应角色并且播发动画的功能。

自定义Track

UMovieSceneTrackInstance

大部分Track的instance类。

继承关系

UMovieSceneSignedObject -> UObject。一个UMovieScene由若干个MasterTrack(UMovieSceneTrack)组成UMovieScene相当于是UMovieSceneTracks的容器那么UMovieSceneTrack又由UMovieSceneSections组成UMovieSceneSection就是一个Track中间的某一段。

UMovieSceneSignedObject

子类有UMovieScene、UMovieSceneSequence、UMovieSceneTrack、UMovieSceneSection。 定义:

  • FGuid Signature用于确定Object身份以便于绑定。
  • FOnSignatureChanged OnSignatureChangedEventChangedEvent。

UMovieSceneSection

UMovieSceneSkeletalAnimationSection

UMovieSceneTrack

UMovieSceneSkeletalAnimationTrack

  • TArray<TObjectPtr<UMovieSceneSection>> AnimationSections存储所有动画Section。
  • FMovieSceneSkeletalAnimRootMotionTrackParams RootMotionParamsRootMotion控制。

编辑器代码:FSkeletalAnimationTrackEditor

Sequence绑定机制笔记

  1. SpawnableObject(自动Spawn的Object)
    表示该Object在Sequence被Evaluate的时候自动Spawn并且由Sequence来管理生命周期
  2. PossessableObject(可以被赋予的Object)
    表示该Object可以被外部赋予比如我手动赋予一个Actor给某个Track

UMovieSceneSequence

UMovieSceneSequence::CreatePossessable() UMovieSceneSequence::BindPossessableObject()

绑定过程可以参考: ULevelSequenceEditorSubsystem::AddActorsToBinding(const TArray<AActor*>& Actors, const FMovieSceneBindingProxy& ObjectBinding)

ActorToSequencer

菜单出现逻辑位于FLevelSequenceEditorActorBinding::BuildSequencerAddMenu(FMenuBuilder& MenuBuilder)。

FLevelSequenceEditorActorBinding::AddPossessActorMenuExtensions(FMenuBuilder& MenuBuilder) => FLevelSequenceEditorActorBinding::AddActorsToSequencer(AActor*const* InActors, int32 NumActors) => TArray<FGuid> FSequencer::AddActors(const TArray<TWeakObjectPtr<AActor> >& InActors, bool bSelectActors) => TArray<FGuid> FSequencerUtilities::AddActors(TSharedRef<ISequencer> Sequencer, const TArray<TWeakObjectPtr<AActor> >& InActors)

TArray<FGuid> FSequencerUtilities::AddActors(TSharedRef<ISequencer> Sequencer, const TArray<TWeakObjectPtr<AActor> >& InActors)
{
	TArray<FGuid> PossessableGuids;

	UMovieSceneSequence* Sequence = Sequencer->GetFocusedMovieSceneSequence();
	if (!Sequence)
	{
		return PossessableGuids;
	}

	UMovieScene* MovieScene = Sequence->GetMovieScene();
	if (!MovieScene)
	{
		return PossessableGuids;
	}

	if (MovieScene->IsReadOnly())
	{
		ShowReadOnlyError();
		return PossessableGuids;
	}
	
	const FScopedTransaction Transaction(LOCTEXT("AddActors", "Add Actors"));
	Sequence->Modify();

	for (TWeakObjectPtr<AActor> WeakActor : InActors)
	{
		if (AActor* Actor = WeakActor.Get())
		{
			FGuid ExistingGuid = Sequencer->FindObjectId(*Actor, Sequencer->GetFocusedTemplateID());
			if (!ExistingGuid.IsValid())
			{
				FGuid PossessableGuid = CreateBinding(Sequencer, *Actor, Actor->GetActorLabel());
				PossessableGuids.Add(PossessableGuid);

				if (ACameraActor* CameraActor = Cast<ACameraActor>(Actor))
				{
					NewCameraAdded(Sequencer, CameraActor, PossessableGuid);
				}

				Sequencer->OnActorAddedToSequencer().Broadcast(Actor, PossessableGuid);
			}
		}
	}

	return PossessableGuids;
}

其他参考

const FGuid PossessableGuid = OwnerMovieScene->AddPossessable(InName, InObject.GetClass());

if (!OwnerMovieScene->FindPossessable(PossessableGuid)->BindSpawnableObject(Sequencer->GetFocusedTemplateID(), &InObject, &Sequencer.Get()))
{
	OwnerSequence->BindPossessableObject(PossessableGuid, InObject, BindingContext);
}
FGuid FSequencerUtilities::CreateBinding(TSharedRef<ISequencer> Sequencer, UObject& InObject, const FString& InName)
{
	const FScopedTransaction Transaction(LOCTEXT("CreateBinding", "Create New Binding"));

	UMovieSceneSequence* OwnerSequence = Sequencer->GetFocusedMovieSceneSequence();
	UMovieScene* OwnerMovieScene = OwnerSequence->GetMovieScene();

	OwnerSequence->Modify();
	OwnerMovieScene->Modify();

	const FGuid PossessableGuid = OwnerMovieScene->AddPossessable(InName, InObject.GetClass());

	// Attempt to use the parent as a context if necessary
	UObject* ParentObject = OwnerSequence->GetParentObject(&InObject);
	UObject* BindingContext = Sequencer->GetPlaybackContext();

	AActor* ParentActorAdded = nullptr;
	FGuid ParentGuid;

	if (ParentObject)
	{
		// Ensure we have possessed the outer object, if necessary
		ParentGuid = Sequencer->GetHandleToObject(ParentObject, false);
		if (!ParentGuid.IsValid())
		{
			ParentGuid = Sequencer->GetHandleToObject(ParentObject);
			ParentActorAdded = Cast<AActor>(ParentObject);
		}

		if (OwnerSequence->AreParentContextsSignificant())
		{
			BindingContext = ParentObject;
		}

		// Set up parent/child guids for possessables within spawnables
		if (ParentGuid.IsValid())
		{
			FMovieScenePossessable* ChildPossessable = OwnerMovieScene->FindPossessable(PossessableGuid);
			if (ensure(ChildPossessable))
			{
				ChildPossessable->SetParent(ParentGuid, OwnerMovieScene);
			}

			FMovieSceneSpawnable* ParentSpawnable = OwnerMovieScene->FindSpawnable(ParentGuid);
			if (ParentSpawnable)
			{
				ParentSpawnable->AddChildPossessable(PossessableGuid);
			}
		}
	}

	if (!OwnerMovieScene->FindPossessable(PossessableGuid)->BindSpawnableObject(Sequencer->GetFocusedTemplateID(), &InObject, &Sequencer.Get()))
	{
		OwnerSequence->BindPossessableObject(PossessableGuid, InObject, BindingContext);
	}

	// Broadcast if a parent actor was added as a result of adding this object
	if (ParentActorAdded && ParentGuid.IsValid())
	{
		Sequencer->OnActorAddedToSequencer().Broadcast(ParentActorAdded, ParentGuid);
	}

	return PossessableGuid;
}

FAudioTrackEditor

FAudioTrackEditor::HandleAssetAdded(UObject* Asset, const FGuid& TargetObjectGuid) => FAudioTrackEditor::AddNewSound() => UMovieSceneAudioTrack::AddNewSoundOnRow(USoundBase* Sound, FFrameNumber Time, int32 RowIndex)

MovieSceneSkeletalAnimationTrack

  • UMovieSceneSkeletalAnimationTrack
  • UMovieSceneSkeletalAnimationSection
    • FMovieSceneSkeletalAnimationParams
      • TObjectPtr<UAnimSequenceBase> Animation
  • FSkeletalAnimationTrackEditor
  • UMovieSceneSkeletalAnimationSystem
  • FSkeletalAnimationTrackEditMode

Track的编辑器注册位于MovieSceneToolsModule.cpp

AnimationTrackCreateEditorHandle = SequencerModule.RegisterTrackEditor( FOnCreateTrackEditor::CreateStatic( &FSkeletalAnimationTrackEditor::CreateTrackEditor ) );

只对UMovieSceneSkeletalAnimationTrack生效

bool FSkeletalAnimationTrackEditor::SupportsSequence(UMovieSceneSequence* InSequence) const  
{  
    ETrackSupport TrackSupported = InSequence ? InSequence->IsTrackSupported(UMovieSceneSkeletalAnimationTrack::StaticClass()) : ETrackSupport::NotSupported;  
    return TrackSupported == ETrackSupport::Supported;  
}

FEditorModeRegistry::Get().RegisterMode<FSkeletalAnimationTrackEditMode>(  
    FSkeletalAnimationTrackEditMode::ModeName,  
    NSLOCTEXT("SkeletalAnimationTrackEditorMode", "SkelAnimTrackEditMode", "Skeletal Anim Track Mode"),  
    FSlateIcon(),  
    false);

FSkeletalAnimationTrackEditor

FSkeletalAnimationTrackEditor::BuildTrackContextMenu()在MovieSceneSkeletalAnimationTrack的Animation上右键出现的菜单。 FSkeletalAnimationTrackEditor::BuildOutlinerEditWidgetMovieSceneSkeletalAnimationTrack下面生成Animtion那一列。

播放逻辑

ALevelSequenceActor::InitializePlayer() 
ALevelSequenceActor->SequencePlayer->Play(); 
ALevelSequenceActor->SequencePlayer->Update(DeltaSeconds);

具体的逻辑位于UMovieSceneSequencePlayer::PlayInternal()


void UMovieSceneSequencePlayer::PlayInternal()
{
	if (NeedsQueueLatentAction())
	{
		QueueLatentAction(FMovieSceneSequenceLatentActionDelegate::CreateUObject(this, &UMovieSceneSequencePlayer::PlayInternal));
		return;
	}

	if (!IsPlaying() && Sequence && CanPlay())
	{
		const FString SequenceName = GetSequenceName(true);
		UE_LOG(LogMovieScene, Verbose, TEXT("PlayInternal - %s (current status: %s)"), *SequenceName, *UEnum::GetValueAsString(Status));

		// Set playback status to playing before any calls to update the position
		Status = EMovieScenePlayerStatus::Playing;

		float PlayRate = bReversePlayback ? -PlaybackSettings.PlayRate : PlaybackSettings.PlayRate;

		// If at the end and playing forwards, rewind to beginning
		if (GetCurrentTime().Time == GetLastValidTime())
		{
			if (PlayRate > 0.f)
			{
				SetPlaybackPosition(FMovieSceneSequencePlaybackParams(FFrameTime(StartTime), EUpdatePositionMethod::Jump));
			}
		}
		else if (GetCurrentTime().Time == FFrameTime(StartTime))
		{
			if (PlayRate < 0.f)
			{
				SetPlaybackPosition(FMovieSceneSequencePlaybackParams(GetLastValidTime(), EUpdatePositionMethod::Jump));
			}
		}

		// Update now
		if (PlaybackSettings.bRestoreState)
		{
			RootTemplateInstance.EnableGlobalPreAnimatedStateCapture();
		}

		bPendingOnStartedPlaying = true;
		Status = EMovieScenePlayerStatus::Playing;
		TimeController->StartPlaying(GetCurrentTime());
		
		if (PlayPosition.GetEvaluationType() == EMovieSceneEvaluationType::FrameLocked)
		{
			if (!OldMaxTickRate.IsSet())
			{
				OldMaxTickRate = GEngine->GetMaxFPS();
			}

			GEngine->SetMaxFPS(1.f / PlayPosition.GetInputRate().AsInterval());
		}

		//播放的核心逻辑估计在这里。
		if (!PlayPosition.GetLastPlayEvalPostition().IsSet() || PlayPosition.GetLastPlayEvalPostition() != PlayPosition.GetCurrentPosition())
		{
			UpdateMovieSceneInstance(PlayPosition.PlayTo(PlayPosition.GetCurrentPosition()), EMovieScenePlayerStatus::Playing);
		}

		RunLatentActions();
		UpdateNetworkSyncProperties();
		
		if (bReversePlayback)
		{
			if (OnPlayReverse.IsBound())
			{
				OnPlayReverse.Broadcast();
			}
		}
		else
		{
			if (OnPlay.IsBound())
			{
				OnPlay.Broadcast();
			}
		}
	}
}

void UMovieSceneSequencePlayer::UpdateMovieSceneInstance(FMovieSceneEvaluationRange InRange, EMovieScenePlayerStatus::Type PlayerStatus, bool bHasJumped)
{
	FMovieSceneUpdateArgs Args;
	Args.bHasJumped = bHasJumped;
	UpdateMovieSceneInstance(InRange, PlayerStatus, Args);
}

void UMovieSceneSequencePlayer::UpdateMovieSceneInstance(FMovieSceneEvaluationRange InRange, EMovieScenePlayerStatus::Type PlayerStatus, const FMovieSceneUpdateArgs& Args)
{
	if (Observer && !Observer->CanObserveSequence())
	{
		return;
	}

	UMovieSceneSequence* MovieSceneSequence = RootTemplateInstance.GetSequence(MovieSceneSequenceID::Root);
	if (!MovieSceneSequence)
	{
		return;
	}

#if !NO_LOGGING
	if (UE_LOG_ACTIVE(LogMovieScene, VeryVerbose))
	{
		const FQualifiedFrameTime CurrentTime = GetCurrentTime();
		const FString SequenceName = GetSequenceName(true);
		UE_LOG(LogMovieScene, VeryVerbose, TEXT("Evaluating sequence %s at frame %d, subframe %f (%f fps)."), *SequenceName, CurrentTime.Time.FrameNumber.Value, CurrentTime.Time.GetSubFrame(), CurrentTime.Rate.AsDecimal());
	}
#endif

	if (PlaybackClient)
	{
		PlaybackClient->WarpEvaluationRange(InRange);
	}

	// Once we have updated we must no longer skip updates
	bSkipNextUpdate = false;

	// We shouldn't be asked to run an async update if we have a blocking sequence.
	check(!Args.bIsAsync || !EnumHasAnyFlags(MovieSceneSequence->GetFlags(), EMovieSceneSequenceFlags::BlockingEvaluation));
	// We shouldn't be asked to run an async update if we don't have a tick manager.
	check(!Args.bIsAsync || TickManager != nullptr);

	FMovieSceneContext Context(InRange, PlayerStatus);
	Context.SetHasJumped(Args.bHasJumped);

	TSharedPtr<FMovieSceneEntitySystemRunner> Runner = RootTemplateInstance.GetRunner();
	if (Runner)
	{
		Runner->QueueUpdate(Context, RootTemplateInstance.GetRootInstanceHandle());
		if (Runner == SynchronousRunner || !Args.bIsAsync)
		{
			Runner->Flush();
		}
	}
}

这段解析来自于:https://zhuanlan.zhihu.com/p/413151867

UMovieSceneSequencePlayer::Play()           //蓝图中调用
UMovieSceneSequencePlayer::PlayInternal()
UMovieSceneSequencePlayer::UpdateMovieSceneInstance
RootTemplateInstance.Evaluate(Context, *this);

// FMovieSceneRootEvaluationTemplateInstance::Evaluate
EntitySystemRunner.Update(Context, RootInstanceHandle);

FMovieSceneEntitySystemRunner::Flush()
FMovieSceneEntitySystemRunner::DoFlushUpdateQueueOnce()
FMovieSceneEntitySystemRunner::GameThread_ProcessQueue()
FMovieSceneEntitySystemRunner::GameThread_SpawnPhase()

// FMovieSceneEntitySystemRunner::GameThread_SpawnPhase()
/// System Linker->GetInstanceRegistry();
FInstanceRegistry* InstanceRegistry = GetInstanceRegistry(); 
FSequenceInstance& Instance = InstanceRegistry->MutateInstance(Update.InstanceHandle);
Instance.Update(Linker, Update.Context);

// FSequenceInstance::Update
SequenceUpdater->Update(Linker, Params, ComponentField, EntitiesScratch);

// FSequenceUpdater_Flat::Update
FSequenceInstance& SequenceInstance = Linker->GetInstanceRegistry()->MutateInstance(InstanceHandle);
SequenceInstance.Ledger.UpdateEntities

// FEntityLedger::UpdateEntities
FInstanceRegistry* InstanceRegistry = Linker->GetInstanceRegistry();
ImportEntity(Linker, ImportParams, EntityField, Query);
}

暂停的核心逻辑:

FMovieSceneEvaluationRange CurrentTimeRange = PlayPosition.GetCurrentPositionAsRange();
const FMovieSceneContext Context(CurrentTimeRange, EMovieScenePlayerStatus::Stopped);
Runner->QueueUpdate(Context, RootTemplateInstance.GetRootInstanceHandle(), FSimpleDelegate::CreateWeakLambda(this, FinishPause));

SequenceRecorder

SequenceRecorder模块的基类是ISequenceRecorder。

Sequencer ECS

使用ECS的原因

可伸缩性
如果使用新的运行时,应该能够编写包含成百上千个轨道或序列的内容,并且将这些内容作为一个整体来优化求值逻辑。这包括:

  • 分配和组织数据使性能不会随着活动Sequencer轨道的增加而快速恶化。
  • 能够完全删除已知不再需要的逻辑和分支不必在每一帧都为它们付出成本。没有代码就是最快的代码。这条原则应该适用于Sequencer求值代码的所有方面而不仅限于最高级。
  • 求值逻辑应该能够直接、高效且不受阻碍地批量访问必要数据,而不必通过复杂或低效的抽象与内存交互。

并发性
写入求值逻辑应该很简单,而且天然就能安全而高效地扩展到多个核心,包括上游/下游依赖性的表达定义(例如,并行地对所有曲线求值)。由于具备仅设置一次管线的综合优点,这不仅能让许多轻量级的小型动画受益,也有益于非常大的序列。

可扩展性
应该可以在内置功能的基础上构建逻辑,而不必重新实现核心系统。添加与核心系统交互的上游或下游功能应该是可以合理实现的。这包括:

  • 在管线中的任何位置,当前帧的所有数据都应该是透明且可改变的。
  • 可靠的依赖性管理。

这些设计目标再加上Sequencer本身的各种问题使得采用数据导向的设计原则成为自然而然的选择,理由如下:

  • 大部分Sequencer数据是同质的可以循序布局。
  • 每种数据变换的逻辑通常具有非常高的独立性与情境无关即运行函数f(x)来得到曲线)。
  • 控制和数据流是非常线性的,没有循环或递归的依赖性(也就是说,没有一种逻辑会要求重新计算已经计算出的数值)。
  • 只有初始设置和最终属性设置器才有线程限制。

可参考Tracker实现

Sequencer 组件相关概念