2290 lines
78 KiB
C++
2290 lines
78 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "MovieSceneSequencePlayer.h"
|
|
#include "MovieScene.h"
|
|
#include "MovieSceneFwd.h"
|
|
#include "MovieSceneTimeHelpers.h"
|
|
#include "MovieSceneSequence.h"
|
|
#include "MovieSceneSequenceTickManager.h"
|
|
#include "Channels/MovieSceneTimeWarpChannel.h"
|
|
#include "Engine/Engine.h"
|
|
#include "UObject/Stack.h"
|
|
#include "Internationalization/Text.h"
|
|
#include "GameFramework/WorldSettings.h"
|
|
#include "Misc/RuntimeErrors.h"
|
|
#include "Net/UnrealNetwork.h"
|
|
#include "Engine/NetDriver.h"
|
|
#include "Engine/NetConnection.h"
|
|
#include "EntitySystem/MovieSceneEntitySystemLinker.h"
|
|
#include "EntitySystem/MovieSceneEntitySystemRunner.h"
|
|
#include "Compilation/MovieSceneCompiledDataManager.h"
|
|
#include "Evaluation/EventTriggerControlPlaybackCapability.h"
|
|
#include "Evaluation/MovieSceneSequenceWeights.h"
|
|
#include "UniversalObjectLocatorResolveParams.h"
|
|
#include "GameFramework/PlayerController.h"
|
|
#include "GameFramework/PlayerState.h"
|
|
#include "Algo/BinarySearch.h"
|
|
#include "UniversalObjectLocatorResolveParameterBuffer.inl"
|
|
|
|
#if UE_WITH_IRIS
|
|
#include "Iris/ReplicationSystem/ReplicationFragmentUtil.h"
|
|
#include "Net/Iris/ReplicationSystem/ReplicationSystemUtil.h"
|
|
#endif // UE_WITH_IRIS
|
|
#include "MovieSceneObjectBindingID.h"
|
|
|
|
|
|
#include UE_INLINE_GENERATED_CPP_BY_NAME(MovieSceneSequencePlayer)
|
|
|
|
DEFINE_LOG_CATEGORY_STATIC(LogMovieSceneRepl, Log, All);
|
|
|
|
DECLARE_STATS_GROUP(TEXT("MovieSceneRepl"), STATGROUP_MovieSceneRepl, STATCAT_Advanced);
|
|
DECLARE_DWORD_COUNTER_STAT(TEXT("NumServerSamples"),MovieSceneRepl_NumServerSamples,STATGROUP_MovieSceneRepl);
|
|
DECLARE_FLOAT_COUNTER_STAT(TEXT("SmoothedServerTime"),MovieSceneRepl_SmoothedServerTime,STATGROUP_MovieSceneRepl);
|
|
|
|
float GSequencerNetSyncThresholdMS = 200;
|
|
static FAutoConsoleVariableRef CVarSequencerNetSyncThresholdMS(
|
|
TEXT("Sequencer.NetSyncThreshold"),
|
|
GSequencerNetSyncThresholdMS,
|
|
TEXT("(Default: 200ms. Defines the threshold at which clients and servers must be forcibly re-synced during playback.")
|
|
);
|
|
|
|
int32 GSequencerMaxSmoothedNetSyncSampleAge = 5000;
|
|
static FAutoConsoleVariableRef CVarSequencerMaxSmoothedNetSyncSampleAge(
|
|
TEXT("Sequencer.SmoothedMaxNetSyncSampleAge"),
|
|
GSequencerMaxSmoothedNetSyncSampleAge,
|
|
TEXT("(Default: 5000. Defines the range of samples (in milliseconds) required to perform smoothed net sync. Use 0 to disable smoothing.")
|
|
);
|
|
|
|
int32 GSequencerMaxSmoothedNetSyncSampleCount = 50;
|
|
static FAutoConsoleVariableRef CVarSequencerMaxSmoothedNetSyncSampleCount(
|
|
TEXT("Sequencer.SmoothedMaxNetSyncSampleCount"),
|
|
GSequencerMaxSmoothedNetSyncSampleCount,
|
|
TEXT("(Default: 50. The maximum number of samples to keep in memory.")
|
|
);
|
|
|
|
float GSequencerSmoothedNetSyncDeviationThreshold = 200;
|
|
static FAutoConsoleVariableRef CVarSequencerSmoothedNetSyncDeviationThreshold(
|
|
TEXT("Sequencer.SmoothedNetSyncDeviationThreshold"),
|
|
GSequencerSmoothedNetSyncDeviationThreshold,
|
|
TEXT("(Default: 200ms. Defines the acceptable deviation for smoothed net sync samples. Samples outside this deviation will be discarded.")
|
|
);
|
|
|
|
int32 GSequencerApplyDisplayRateToDynResFrameTimeBudget = 0;
|
|
static FAutoConsoleVariableRef CVarSequencerApplyDisplayRateToDynResFrameTimeBudget(
|
|
TEXT("Sequencer.ApplyDisplayRateToDynamicResolutionFrameTimeBudget"),
|
|
GSequencerApplyDisplayRateToDynResFrameTimeBudget,
|
|
TEXT("(Whether to override r.DynamicRes.FrameTimeBudget based on sequence display rate when using 'Lock to Display Rate at Runtime'.")
|
|
);
|
|
|
|
bool FMovieSceneSequenceLoopCount::SerializeFromMismatchedTag( const FPropertyTag& Tag, FStructuredArchive::FSlot Slot )
|
|
{
|
|
if (Tag.Type == NAME_IntProperty)
|
|
{
|
|
Slot << Value;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool FMovieSceneSequencePlaybackSettings::SerializeFromMismatchedTag( const FPropertyTag& Tag, FStructuredArchive::FSlot Slot )
|
|
{
|
|
if (Tag.GetType().IsStruct("LevelSequencePlaybackSettings"))
|
|
{
|
|
StaticStruct()->SerializeItem(Slot, this, nullptr);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
FFrameTime FMovieSceneSequencePlaybackParams::GetPlaybackPosition(UMovieSceneSequencePlayer* Player) const
|
|
{
|
|
FFrameTime PlaybackPosition = Player->GetCurrentTime().Time;
|
|
|
|
if (PositionType == EMovieScenePositionType::Frame)
|
|
{
|
|
PlaybackPosition = Frame;
|
|
}
|
|
else if (PositionType == EMovieScenePositionType::Time)
|
|
{
|
|
PlaybackPosition = Time * Player->GetFrameRate();
|
|
}
|
|
else if (PositionType == EMovieScenePositionType::MarkedFrame)
|
|
{
|
|
UMovieScene* MovieScene = Player->GetSequence() ? Player->GetSequence()->GetMovieScene() : nullptr;
|
|
|
|
if (MovieScene)
|
|
{
|
|
int32 MarkedIndex = MovieScene->FindMarkedFrameByLabel(MarkedFrame);
|
|
|
|
if (MarkedIndex != INDEX_NONE)
|
|
{
|
|
PlaybackPosition = ConvertFrameTime(MovieScene->GetMarkedFrames()[MarkedIndex].FrameNumber, MovieScene->GetTickResolution(), MovieScene->GetDisplayRate());
|
|
}
|
|
}
|
|
}
|
|
else if (PositionType == EMovieScenePositionType::Timecode)
|
|
{
|
|
PlaybackPosition = Timecode.ToFrameNumber(Player->GetFrameRate());
|
|
}
|
|
|
|
return PlaybackPosition;
|
|
}
|
|
|
|
FFrameTime FMovieSceneSequencePlaybackParams::GetPlaybackPosition(UMovieSceneSequence* Sequence) const
|
|
{
|
|
FFrameTime PlaybackPosition;
|
|
|
|
UMovieScene* MovieScene = Sequence->GetMovieScene();
|
|
if (!MovieScene)
|
|
{
|
|
return PlaybackPosition;
|
|
}
|
|
|
|
FFrameRate DisplayRate = MovieScene->GetDisplayRate();
|
|
FFrameRate TickResolution = MovieScene->GetTickResolution();
|
|
|
|
if (PositionType == EMovieScenePositionType::Frame)
|
|
{
|
|
PlaybackPosition = Frame;
|
|
}
|
|
else if (PositionType == EMovieScenePositionType::Time)
|
|
{
|
|
PlaybackPosition = Time * DisplayRate;
|
|
}
|
|
else if (PositionType == EMovieScenePositionType::MarkedFrame)
|
|
{
|
|
int32 MarkedIndex = MovieScene->FindMarkedFrameByLabel(MarkedFrame);
|
|
|
|
if (MarkedIndex != INDEX_NONE)
|
|
{
|
|
PlaybackPosition = ConvertFrameTime(MovieScene->GetMarkedFrames()[MarkedIndex].FrameNumber, TickResolution, DisplayRate);
|
|
}
|
|
}
|
|
else if (PositionType == EMovieScenePositionType::Timecode)
|
|
{
|
|
PlaybackPosition = Timecode.ToFrameNumber(DisplayRate);
|
|
}
|
|
|
|
return PlaybackPosition;
|
|
}
|
|
|
|
UMovieSceneSequencePlayer::UMovieSceneSequencePlayer(const FObjectInitializer& Init)
|
|
: Super(Init)
|
|
, Status(EMovieScenePlayerStatus::Stopped)
|
|
, bReversePlayback(false)
|
|
, bPendingOnStartedPlaying(false)
|
|
, TimeControllerState(ETimeControllerState::ReadyToPlay)
|
|
, bIsAsyncUpdate(false)
|
|
, bSkipNextUpdate(false)
|
|
, bUpdateNetSync(false)
|
|
, bWarnZeroDuration(true)
|
|
, Sequence(nullptr)
|
|
, StartTime(0)
|
|
, DurationFrames(0)
|
|
, DurationSubFrames(0.f)
|
|
, CurrentNumLoops(0)
|
|
, SerialNumber(0)
|
|
, CurrentRunner(nullptr)
|
|
{
|
|
PlayPosition.Reset(FFrameTime(0));
|
|
|
|
NetSyncProps.LastKnownPosition = FFrameTime(0);
|
|
NetSyncProps.LastKnownStatus = Status;
|
|
}
|
|
|
|
UMovieSceneSequencePlayer::~UMovieSceneSequencePlayer()
|
|
{
|
|
if (GEngine && OldMaxTickRate.IsSet())
|
|
{
|
|
GEngine->SetMaxFPS(OldMaxTickRate.GetValue());
|
|
}
|
|
|
|
if (bOverridingDynResFrameTimeBudget)
|
|
{
|
|
static IConsoleVariable* CVarDynResFrameTimeBudget = IConsoleManager::Get().FindConsoleVariable(TEXT("r.DynamicRes.FrameTimeBudget"));
|
|
CVarDynResFrameTimeBudget->Unset(ECVF_SetByCode);
|
|
|
|
bOverridingDynResFrameTimeBudget = false;
|
|
}
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::UpdateNetworkSyncProperties()
|
|
{
|
|
if (HasAuthority())
|
|
{
|
|
NetSyncProps.LastKnownPosition = PlayPosition.GetCurrentPosition();
|
|
NetSyncProps.LastKnownStatus = Status;
|
|
NetSyncProps.LastKnownNumLoops = CurrentNumLoops;
|
|
NetSyncProps.LastKnownSerialNumber = SerialNumber;
|
|
}
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
|
|
{
|
|
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
|
|
|
|
DOREPLIFETIME(UMovieSceneSequencePlayer, NetSyncProps);
|
|
DOREPLIFETIME(UMovieSceneSequencePlayer, bReversePlayback);
|
|
DOREPLIFETIME(UMovieSceneSequencePlayer, StartTime);
|
|
DOREPLIFETIME(UMovieSceneSequencePlayer, DurationFrames);
|
|
DOREPLIFETIME(UMovieSceneSequencePlayer, DurationSubFrames);
|
|
DOREPLIFETIME(UMovieSceneSequencePlayer, PlaybackSettings);
|
|
DOREPLIFETIME(UMovieSceneSequencePlayer, Observer);
|
|
}
|
|
|
|
EMovieScenePlayerStatus::Type UMovieSceneSequencePlayer::GetPlaybackStatus() const
|
|
{
|
|
return Status;
|
|
}
|
|
|
|
FMovieSceneSpawnRegister& UMovieSceneSequencePlayer::GetSpawnRegister()
|
|
{
|
|
return SpawnRegister.IsValid() ? *SpawnRegister : IMovieScenePlayer::GetSpawnRegister();
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::ResolveBoundObjects(UE::UniversalObjectLocator::FResolveParams& ResolveParams, const FGuid& InBindingId, FMovieSceneSequenceID SequenceID, UMovieSceneSequence& InSequence, TArray<UObject*, TInlineAllocator<1>>& OutObjects) const
|
|
{
|
|
using namespace UE::MovieScene;
|
|
bool bAllowDefault = PlaybackClient ? PlaybackClient->RetrieveBindingOverrides(InBindingId, SequenceID, OutObjects) : true;
|
|
|
|
if (bAllowDefault)
|
|
{
|
|
InSequence.LocateBoundObjects(InBindingId, ResolveParams, FindSharedPlaybackState(), OutObjects);
|
|
}
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::Play()
|
|
{
|
|
if (NeedsQueueLatentAction())
|
|
{
|
|
QueueLatentAction(FMovieSceneSequenceLatentActionDelegate::CreateUObject(this, &UMovieSceneSequencePlayer::Play));
|
|
return;
|
|
}
|
|
|
|
bReversePlayback = false;
|
|
PlayInternal();
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::PlayReverse()
|
|
{
|
|
if (NeedsQueueLatentAction())
|
|
{
|
|
QueueLatentAction(FMovieSceneSequenceLatentActionDelegate::CreateUObject(this, &UMovieSceneSequencePlayer::PlayReverse));
|
|
return;
|
|
}
|
|
|
|
bReversePlayback = true;
|
|
PlayInternal();
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::ChangePlaybackDirection()
|
|
{
|
|
if (NeedsQueueLatentAction())
|
|
{
|
|
QueueLatentAction(FMovieSceneSequenceLatentActionDelegate::CreateUObject(this, &UMovieSceneSequencePlayer::ChangePlaybackDirection));
|
|
return;
|
|
}
|
|
|
|
bReversePlayback = !bReversePlayback;
|
|
PlayInternal();
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::PlayLooping(int32 NumLoops)
|
|
{
|
|
if (NeedsQueueLatentAction())
|
|
{
|
|
QueueLatentAction(FMovieSceneSequenceLatentActionDelegate::CreateUObject(this, &UMovieSceneSequencePlayer::PlayLooping, NumLoops));
|
|
return;
|
|
}
|
|
|
|
PlaybackSettings.LoopCount.Value = NumLoops;
|
|
PlayInternal();
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::PlayInternal()
|
|
{
|
|
if (Observer && !Observer->CanObserveSequence())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (NeedsQueueLatentAction())
|
|
{
|
|
QueueLatentAction(FMovieSceneSequenceLatentActionDelegate::CreateUObject(this, &UMovieSceneSequencePlayer::PlayInternal));
|
|
return;
|
|
}
|
|
|
|
if (!IsPlaying() && Sequence && CanPlay() && TimeControllerState == ETimeControllerState::ReadyToPlay)
|
|
{
|
|
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.FinishCompletionStateOverride == EMovieSceneCompletionModeOverride::ForceRestoreState)
|
|
{
|
|
RootTemplateInstance.EnableGlobalPreAnimatedStateCapture();
|
|
}
|
|
|
|
TimeController->PrepareToPlay(GetCurrentTime());
|
|
if (TimeController->IsReadyToPlay())
|
|
{
|
|
StartTimeControllerAndBroadcastPlayState();
|
|
TimeControllerState = ETimeControllerState::ReadyToPlay;
|
|
}
|
|
else
|
|
{
|
|
TimeControllerState = ETimeControllerState::PreparingToPlay;
|
|
}
|
|
}
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::Pause()
|
|
{
|
|
if (Observer && !Observer->CanObserveSequence())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (NeedsQueueLatentAction())
|
|
{
|
|
QueueLatentAction(FMovieSceneSequenceLatentActionDelegate::CreateUObject(this, &UMovieSceneSequencePlayer::Pause));
|
|
return;
|
|
}
|
|
|
|
const FString SequenceName = GetSequenceName(true);
|
|
UE_LOG(LogMovieScene, Verbose, TEXT("Pause - %s (current status: %s)"), *SequenceName, *UEnum::GetValueAsString(Status));
|
|
|
|
if (IsPlaying())
|
|
{
|
|
Status = EMovieScenePlayerStatus::Paused;
|
|
TimeController->StopPlaying(GetCurrentTime());
|
|
|
|
PauseOnFrame.Reset();
|
|
LastTickGameTimeSeconds.Reset();
|
|
|
|
auto FinishPause = [this]
|
|
{
|
|
this->RunLatentActions();
|
|
this->UpdateNetworkSyncProperties();
|
|
|
|
const FString SequenceName = this->GetSequenceName(true);
|
|
UE_LOG(LogMovieScene, Verbose, TEXT("Paused - %s"), *SequenceName);
|
|
|
|
if (this->OnPause.IsBound())
|
|
{
|
|
this->OnPause.Broadcast();
|
|
}
|
|
};
|
|
|
|
// Evaluate the sequence at its current time, with a status of 'stopped' to ensure that animated state pauses correctly. (ie. audio sounds should stop/pause)
|
|
if (TSharedPtr<FMovieSceneEntitySystemRunner> Runner = RootTemplateInstance.GetRunner())
|
|
{
|
|
FMovieSceneEvaluationRange CurrentTimeRange = PlayPosition.GetCurrentPositionAsRange();
|
|
|
|
if (PlaybackClient)
|
|
{
|
|
PlaybackClient->WarpEvaluationRange(CurrentTimeRange);
|
|
}
|
|
|
|
const FMovieSceneContext Context(CurrentTimeRange, EMovieScenePlayerStatus::Stopped);
|
|
|
|
Runner->QueueUpdate(Context, RootTemplateInstance.GetRootInstanceHandle(), FSimpleDelegate::CreateWeakLambda(this, FinishPause));
|
|
}
|
|
else
|
|
{
|
|
FinishPause();
|
|
}
|
|
}
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::Scrub()
|
|
{
|
|
Status = EMovieScenePlayerStatus::Scrubbing;
|
|
TimeController->StopPlaying(GetCurrentTime());
|
|
|
|
UpdateNetworkSyncProperties();
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::Stop()
|
|
{
|
|
StopInternal(bReversePlayback ? GetLastValidTime() : FFrameTime(StartTime));
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::StopAtCurrentTime()
|
|
{
|
|
StopInternal(PlayPosition.GetCurrentPosition());
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::StopInternal(FFrameTime TimeToResetTo)
|
|
{
|
|
if (Observer && !Observer->CanObserveSequence())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (NeedsQueueLatentAction())
|
|
{
|
|
QueueLatentAction(FMovieSceneSequenceLatentActionDelegate::CreateUObject(this, &UMovieSceneSequencePlayer::StopInternal, TimeToResetTo));
|
|
return;
|
|
}
|
|
|
|
const FString SequenceName = GetSequenceName(true);
|
|
UE_LOG(LogMovieScene, Verbose, TEXT("StopInternal - %s (at: %s, current status: %s)"), *SequenceName, *LexToString(TimeToResetTo), *UEnum::GetValueAsString(Status));
|
|
|
|
// We may have been waiting for our time controller to tell us it was ready to play,
|
|
// so TimeControllerState may be ETimeControllerState::PreparingToPlay. Here we reset that back to its
|
|
// initialized state of ETimeControllerState::ReadyToPlay.
|
|
TimeControllerState = ETimeControllerState::ReadyToPlay;
|
|
|
|
if (IsPlaying() || IsPaused())
|
|
{
|
|
Status = EMovieScenePlayerStatus::Stopped;
|
|
|
|
// Put the cursor at the specified position
|
|
PlayPosition.Reset(TimeToResetTo);
|
|
if (TimeController.IsValid())
|
|
{
|
|
TimeController->StopPlaying(GetCurrentTime());
|
|
}
|
|
|
|
CurrentNumLoops = 0;
|
|
PauseOnFrame.Reset();
|
|
LastTickGameTimeSeconds.Reset();
|
|
|
|
// Reset loop count on stop so that it doesn't persist to the next call to play
|
|
PlaybackSettings.LoopCount.Value = 0;
|
|
|
|
if (PlaybackSettings.FinishCompletionStateOverride == EMovieSceneCompletionModeOverride::ForceRestoreState)
|
|
{
|
|
RestorePreAnimatedState();
|
|
}
|
|
else if (PlaybackSettings.FinishCompletionStateOverride == EMovieSceneCompletionModeOverride::ForceKeepState)
|
|
{
|
|
DiscardPreAnimatedState();
|
|
}
|
|
|
|
// Lambda that is invoked when the request to finish this sequence has been fulfilled
|
|
auto OnFlushed = [this, TimeToResetTo]
|
|
{
|
|
if (this->OldMaxTickRate.IsSet())
|
|
{
|
|
GEngine->SetMaxFPS(OldMaxTickRate.GetValue());
|
|
this->OldMaxTickRate.Reset();
|
|
}
|
|
|
|
if (bOverridingDynResFrameTimeBudget)
|
|
{
|
|
static IConsoleVariable* CVarDynResFrameTimeBudget = IConsoleManager::Get().FindConsoleVariable(TEXT("r.DynamicRes.FrameTimeBudget"));
|
|
CVarDynResFrameTimeBudget->Unset(ECVF_SetByCode);
|
|
|
|
bOverridingDynResFrameTimeBudget = false;
|
|
}
|
|
|
|
this->UpdateNetworkSyncProperties();
|
|
|
|
const FString SequenceName = this->GetSequenceName(true);
|
|
UE_LOG(LogMovieScene, Verbose, TEXT("Stopped - %s"), *SequenceName);
|
|
|
|
if (this->HasAuthority())
|
|
{
|
|
// Explicitly handle Stop() events through an RPC call
|
|
this->RPC_OnStopEvent(TimeToResetTo, SerialNumber + 1);
|
|
}
|
|
|
|
this->OnStopped();
|
|
|
|
if (this->OnStop.IsBound())
|
|
{
|
|
this->OnStop.Broadcast();
|
|
}
|
|
|
|
this->RunLatentActions();
|
|
};
|
|
|
|
TSharedPtr<FMovieSceneEntitySystemRunner> Runner = RootTemplateInstance.GetRunner();
|
|
|
|
if (Runner)
|
|
{
|
|
// Finish but do not destroy
|
|
if (Runner->QueueFinalUpdate(RootTemplateInstance.GetRootInstanceHandle(), FSimpleDelegate::CreateWeakLambda(this, OnFlushed)))
|
|
{
|
|
Runner->Flush();
|
|
}
|
|
}
|
|
}
|
|
else if (RootTemplateInstance.IsValid() && RootTemplateInstance.HasEverUpdated())
|
|
{
|
|
if (PlaybackSettings.FinishCompletionStateOverride == EMovieSceneCompletionModeOverride::ForceRestoreState)
|
|
{
|
|
RestorePreAnimatedState();
|
|
}
|
|
else if (PlaybackSettings.FinishCompletionStateOverride == EMovieSceneCompletionModeOverride::ForceKeepState)
|
|
{
|
|
DiscardPreAnimatedState();
|
|
}
|
|
|
|
TSharedPtr<FMovieSceneEntitySystemRunner> Runner = RootTemplateInstance.GetRunner();
|
|
if (Runner)
|
|
{
|
|
// Finish but do not destroy
|
|
if (Runner->QueueFinalUpdate(RootTemplateInstance.GetRootInstanceHandle()))
|
|
{
|
|
Runner->Flush();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::FinishPlaybackInternal(FFrameTime TimeToFinishAt)
|
|
{
|
|
if (PlaybackSettings.bPauseAtEnd)
|
|
{
|
|
Pause();
|
|
}
|
|
else
|
|
{
|
|
StopInternal(TimeToFinishAt);
|
|
}
|
|
|
|
TimeController->StopPlaying(GetCurrentTime());
|
|
|
|
if (OnFinished.IsBound())
|
|
{
|
|
OnFinished.Broadcast();
|
|
}
|
|
|
|
OnNativeFinished.ExecuteIfBound();
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::GoToEndAndStop()
|
|
{
|
|
FFrameTime LastValidTime = GetLastValidTime();
|
|
|
|
if (PlayPosition.GetCurrentPosition() == LastValidTime && Status == EMovieScenePlayerStatus::Stopped)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Status = EMovieScenePlayerStatus::Playing;
|
|
SetPlaybackPosition(FMovieSceneSequencePlaybackParams(LastValidTime, EUpdatePositionMethod::Jump));
|
|
StopInternal(LastValidTime);
|
|
}
|
|
|
|
FQualifiedFrameTime UMovieSceneSequencePlayer::GetCurrentTime() const
|
|
{
|
|
FFrameTime Time = PlayPosition.GetCurrentPosition();
|
|
return FQualifiedFrameTime(Time, PlayPosition.GetInputRate());
|
|
}
|
|
|
|
FQualifiedFrameTime UMovieSceneSequencePlayer::GetDuration() const
|
|
{
|
|
return FQualifiedFrameTime(FFrameTime(DurationFrames, DurationSubFrames), PlayPosition.GetInputRate());
|
|
}
|
|
|
|
int32 UMovieSceneSequencePlayer::GetFrameDuration() const
|
|
{
|
|
return DurationFrames;
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::SetFrameRate(FFrameRate FrameRate)
|
|
{
|
|
if (!FrameRate.IsValid() || FrameRate.Numerator <= 0)
|
|
{
|
|
const FString SequenceName = GetSequenceName(true);
|
|
UE_LOG(LogMovieScene, Error, TEXT("Attempting to set sequence %s with an invalid frame rate: %s"), *SequenceName, *FrameRate.ToPrettyText().ToString());
|
|
return;
|
|
}
|
|
|
|
UMovieScene* MovieScene = Sequence ? Sequence->GetMovieScene() : nullptr;
|
|
if (MovieScene)
|
|
{
|
|
if (MovieScene->GetEvaluationType() == EMovieSceneEvaluationType::FrameLocked && !FrameRate.IsMultipleOf(MovieScene->GetTickResolution()))
|
|
{
|
|
UE_LOG(LogMovieScene, Warning, TEXT("Attempting to play back a sequence with tick resolution of %f ticks per second frame locked to %f fps, which is not a multiple of the resolution."), MovieScene->GetTickResolution().AsDecimal(), FrameRate.AsDecimal());
|
|
}
|
|
}
|
|
|
|
FFrameRate CurrentInputRate = PlayPosition.GetInputRate();
|
|
|
|
StartTime = ConvertFrameTime(StartTime, CurrentInputRate, FrameRate).FloorToFrame();
|
|
DurationFrames = ConvertFrameTime(FFrameNumber(DurationFrames), CurrentInputRate, FrameRate).RoundToFrame().Value;
|
|
|
|
PlayPosition.SetTimeBase(FrameRate, PlayPosition.GetOutputRate(), PlayPosition.GetEvaluationType());
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::SetFrameRange( int32 NewStartTime, int32 Duration, float SubFrames )
|
|
{
|
|
Duration = FMath::Max(Duration, 0);
|
|
|
|
StartTime = NewStartTime;
|
|
DurationFrames = Duration;
|
|
DurationSubFrames = SubFrames;
|
|
|
|
TOptional<FFrameTime> CurrentTime = PlayPosition.GetCurrentPosition();
|
|
if (CurrentTime.IsSet())
|
|
{
|
|
FFrameTime LastValidTime = GetLastValidTime();
|
|
|
|
if (CurrentTime.GetValue() < StartTime)
|
|
{
|
|
PlayPosition.Reset(StartTime);
|
|
}
|
|
else if (CurrentTime.GetValue() > LastValidTime)
|
|
{
|
|
PlayPosition.Reset(LastValidTime);
|
|
}
|
|
}
|
|
|
|
if (TimeController.IsValid())
|
|
{
|
|
TimeController->Reset(GetCurrentTime());
|
|
}
|
|
|
|
UpdateNetworkSyncProperties();
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::SetTimeRange( float StartTimeSeconds, float DurationSeconds )
|
|
{
|
|
const FFrameRate Rate = PlayPosition.GetInputRate();
|
|
|
|
const FFrameNumber StartFrame = ( StartTimeSeconds * Rate ).FloorToFrame();
|
|
const FFrameNumber Duration = ( DurationSeconds * Rate ).RoundToFrame();
|
|
|
|
SetFrameRange(StartFrame.Value, Duration.Value);
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::PlayTo(FMovieSceneSequencePlaybackParams InPlaybackParams, FMovieSceneSequencePlayToParams PlayToParams)
|
|
{
|
|
FPauseOnArgs Args;
|
|
Args.Time = InPlaybackParams.GetPlaybackPosition(this);
|
|
Args.bExclusive = PlayToParams.bExclusive;
|
|
PauseOnFrame = Args;
|
|
|
|
if (GetCurrentTime().Time < Args.Time)
|
|
{
|
|
Play();
|
|
}
|
|
else
|
|
{
|
|
PlayReverse();
|
|
}
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::SetPlaybackPosition(FMovieSceneSequencePlaybackParams InPlaybackParams)
|
|
{
|
|
if (Observer && !Observer->CanObserveSequence())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!Sequence)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (NeedsQueueLatentAction())
|
|
{
|
|
QueueLatentAction(FMovieSceneSequenceLatentActionDelegate::CreateUObject(this, &UMovieSceneSequencePlayer::SetPlaybackPosition, InPlaybackParams));
|
|
return;
|
|
}
|
|
|
|
FFrameTime NewPosition = InPlaybackParams.GetPlaybackPosition(this);
|
|
|
|
UpdateTimeCursorPosition(NewPosition, InPlaybackParams.UpdateMethod, InPlaybackParams.bHasJumped);
|
|
|
|
TimeController->Reset(GetCurrentTime());
|
|
|
|
if (HasAuthority())
|
|
{
|
|
RPC_ExplicitServerUpdateEvent(InPlaybackParams.UpdateMethod, NewPosition, SerialNumber + 1);
|
|
}
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::RestoreState()
|
|
{
|
|
if (PlaybackSettings.FinishCompletionStateOverride != EMovieSceneCompletionModeOverride::ForceRestoreState)
|
|
{
|
|
UE_LOG(LogMovieScene, Warning, TEXT("Attempting to restore pre-animated state for a player that was not set to capture pre-animated state. Please set PlaybackSettings.FinishCompletionStateOverride to ForceRestoreState"));
|
|
}
|
|
|
|
RestorePreAnimatedState();
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::SetCompletionModeOverride(EMovieSceneCompletionModeOverride CompletionModeOverride)
|
|
{
|
|
if (IsPlaying() && PlaybackSettings.FinishCompletionStateOverride != EMovieSceneCompletionModeOverride::ForceRestoreState && CompletionModeOverride == EMovieSceneCompletionModeOverride::ForceRestoreState)
|
|
{
|
|
UE_LOG(LogMovieScene, Warning, TEXT("Attempting to set completion mode override to force restore state while the sequence is already playing. Force restore state must be set before starting playback."));
|
|
}
|
|
|
|
PlaybackSettings.FinishCompletionStateOverride = CompletionModeOverride;
|
|
}
|
|
|
|
EMovieSceneCompletionModeOverride UMovieSceneSequencePlayer::GetCompletionModeOverride() const
|
|
{
|
|
return PlaybackSettings.FinishCompletionStateOverride;
|
|
}
|
|
|
|
bool UMovieSceneSequencePlayer::IsPlaying() const
|
|
{
|
|
return Status == EMovieScenePlayerStatus::Playing;
|
|
}
|
|
|
|
bool UMovieSceneSequencePlayer::IsPaused() const
|
|
{
|
|
return Status == EMovieScenePlayerStatus::Paused;
|
|
}
|
|
|
|
bool UMovieSceneSequencePlayer::IsReversed() const
|
|
{
|
|
return bReversePlayback;
|
|
}
|
|
|
|
float UMovieSceneSequencePlayer::GetPlayRate() const
|
|
{
|
|
return PlaybackSettings.PlayRate;
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::SetPlayRate(float PlayRate)
|
|
{
|
|
PlaybackSettings.PlayRate = PlayRate;
|
|
}
|
|
|
|
bool UMovieSceneSequencePlayer::GetHideHud() const
|
|
{
|
|
return PlaybackSettings.bHideHud;
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::SetHideHud(bool HideHud)
|
|
{
|
|
PlaybackSettings.bHideHud = HideHud;
|
|
}
|
|
|
|
FFrameTime UMovieSceneSequencePlayer::GetLastValidTime() const
|
|
{
|
|
if (DurationFrames > 0)
|
|
{
|
|
if (DurationSubFrames > 0.f)
|
|
{
|
|
return FFrameTime(StartTime + DurationFrames, DurationSubFrames);
|
|
}
|
|
else
|
|
{
|
|
return FFrameTime(StartTime + DurationFrames - 1, 0.99999994f);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return FFrameTime(StartTime);
|
|
}
|
|
}
|
|
|
|
FFrameRate UMovieSceneSequencePlayer::GetDisplayRate() const
|
|
{
|
|
return Sequence && Sequence->GetMovieScene() ? Sequence->GetMovieScene()->GetDisplayRate() : FFrameRate();
|
|
}
|
|
|
|
bool UMovieSceneSequencePlayer::ShouldStopOrLoop(FFrameTime NewPosition) const
|
|
{
|
|
bool bShouldStopOrLoop = false;
|
|
if (IsPlaying())
|
|
{
|
|
if (!bReversePlayback)
|
|
{
|
|
bShouldStopOrLoop = NewPosition >= FFrameTime(StartTime + GetFrameDuration(), DurationSubFrames);
|
|
}
|
|
else
|
|
{
|
|
bShouldStopOrLoop = NewPosition.FrameNumber < StartTime;
|
|
}
|
|
}
|
|
return bShouldStopOrLoop;
|
|
}
|
|
|
|
TOptional<TRange<FFrameTime>> UMovieSceneSequencePlayer::GetPauseRange(const FFrameTime& NewPosition) const
|
|
{
|
|
if (IsPlaying() && PauseOnFrame.IsSet())
|
|
{
|
|
TRange<FFrameTime> OutRange;
|
|
|
|
// NewPosition and PauseOnFrame are stored in Display Rate, but we need to convert to tick resolution for the actual ranges.
|
|
FFrameTime PauseTime = FFrameRate::TransformTime(PauseOnFrame->Time, PlayPosition.GetInputRate(), PlayPosition.GetOutputRate());
|
|
FFrameTime CurrentTime = FFrameRate::TransformTime(PlayPosition.GetCurrentPosition(), PlayPosition.GetInputRate(), PlayPosition.GetOutputRate());
|
|
FFrameTime NewTime = FFrameRate::TransformTime(NewPosition, PlayPosition.GetInputRate(), PlayPosition.GetOutputRate());
|
|
|
|
// This logic is a little complicated due to inclusive/exclusive range times for pausing. Inclusive/Exclusive only have any meaning
|
|
// with direction (as we need direction to decide which bound to make inclusive/exclusive).
|
|
if (bReversePlayback)
|
|
{
|
|
|
|
// If we're going in reverse then the start frame is the exclusive one
|
|
OutRange = TRange<FFrameTime>(PauseOnFrame->bExclusive
|
|
? TRangeBound<FFrameTime>::Inclusive(PauseTime + FFrameTime(FFrameNumber(1)))
|
|
: TRangeBound<FFrameTime>::Inclusive(PauseTime),
|
|
TRangeBound<FFrameTime>::Inclusive(CurrentTime));
|
|
|
|
}
|
|
else
|
|
{
|
|
// If we're playing forward then the upper bound is (potentially) exclusive
|
|
OutRange = TRange<FFrameTime>(TRangeBound<FFrameTime>::Inclusive(CurrentTime),
|
|
PauseOnFrame->bExclusive
|
|
? TRangeBound<FFrameTime>::Exclusive(PauseTime)
|
|
: TRangeBound<FFrameTime>::Exclusive(PauseTime + FFrameTime(FFrameNumber(1))));
|
|
|
|
}
|
|
|
|
// If the new time is outside of bounds, then we should pause.
|
|
if (!OutRange.Contains(NewTime))
|
|
{
|
|
return OutRange;
|
|
}
|
|
}
|
|
|
|
return TOptional<TRange<FFrameTime>>();
|
|
}
|
|
|
|
UMovieSceneEntitySystemLinker* UMovieSceneSequencePlayer::ConstructEntitySystemLinker()
|
|
{
|
|
if (ensure(TickManager && RegisteredTickInterval.IsSet()) && !EnumHasAnyFlags(Sequence->GetFlags(), EMovieSceneSequenceFlags::BlockingEvaluation))
|
|
{
|
|
return TickManager->GetLinker(RegisteredTickInterval.GetValue());
|
|
}
|
|
|
|
return UMovieSceneEntitySystemLinker::CreateLinker(GetPlaybackContext(), UE::MovieScene::EEntitySystemLinkerRole::Standalone);
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::InitializeForTick(UObject* Context)
|
|
{
|
|
// Store a reference to the global tick manager to keep it alive while there are sequence players active.
|
|
if (ensure(Context))
|
|
{
|
|
TickManager = UMovieSceneSequenceTickManager::Get(Context);
|
|
}
|
|
}
|
|
|
|
|
|
void UMovieSceneSequencePlayer::SetPlaybackSettings(const FMovieSceneSequencePlaybackSettings& InSettings)
|
|
{
|
|
PlaybackSettings = InSettings;
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::Initialize(UMovieSceneSequence* InSequence, const FMovieSceneSequencePlaybackSettings& InSettings)
|
|
{
|
|
PlaybackSettings = InSettings;
|
|
Initialize(InSequence);
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::Initialize(UMovieSceneSequence* InSequence)
|
|
{
|
|
check(InSequence);
|
|
check(!IsEvaluating());
|
|
|
|
// If we have a valid sequence that may have been played back,
|
|
// Explicitly stop and tear down the template instance before
|
|
// reinitializing it with the new sequence. Care should be taken
|
|
// here that Stop is not called on the first Initialization as this
|
|
// may be called during PostLoad.
|
|
if (Sequence)
|
|
{
|
|
StopAtCurrentTime();
|
|
}
|
|
|
|
Sequence = InSequence;
|
|
|
|
FFrameTime StartTimeWithOffset = StartTime;
|
|
|
|
EUpdateClockSource ClockToUse = EUpdateClockSource::Tick;
|
|
|
|
UMovieScene* MovieScene = Sequence->GetMovieScene();
|
|
if (MovieScene)
|
|
{
|
|
EMovieSceneEvaluationType EvaluationType = MovieScene->GetEvaluationType();
|
|
FFrameRate TickResolution = MovieScene->GetTickResolution();
|
|
FFrameRate DisplayRate = MovieScene->GetDisplayRate();
|
|
|
|
UE_LOG(LogMovieScene, Verbose, TEXT("Initialize - MovieSceneSequence: %s, TickResolution: %s, DisplayRate: %s"), *InSequence->GetName(), *TickResolution.ToPrettyText().ToString(), *DisplayRate.ToPrettyText().ToString());
|
|
|
|
if (!TickResolution.IsValid() || TickResolution.Numerator <= 0)
|
|
{
|
|
const FString SequenceName = GetSequenceName(true);
|
|
const FFrameRate DefaultTickResolution(60000, 1);
|
|
UE_LOG(LogMovieScene, Error, TEXT("Attempting to set sequence %s with an invalid tick resolution: %s, defaulting to: %s"), *SequenceName, *TickResolution.ToPrettyText().ToString(), *DefaultTickResolution.ToPrettyText().ToString());
|
|
TickResolution = DefaultTickResolution;
|
|
}
|
|
|
|
if (!DisplayRate.IsValid() || DisplayRate.Numerator <= 0)
|
|
{
|
|
const FString SequenceName = GetSequenceName(true);
|
|
const FFrameRate DefaultDisplayRate(30, 1);
|
|
UE_LOG(LogMovieScene, Error, TEXT("Attempting to set sequence %s with an invalid display rate: %s, defaulting to: %s"), *SequenceName, *DisplayRate.ToPrettyText().ToString(), *DefaultDisplayRate.ToPrettyText().ToString());
|
|
DisplayRate = DefaultDisplayRate;
|
|
}
|
|
|
|
// We set the play position in terms of the display rate,
|
|
// but want evaluation ranges in the moviescene's tick resolution
|
|
PlayPosition.SetTimeBase(DisplayRate, TickResolution, EvaluationType);
|
|
|
|
{
|
|
// Set up the default frame range from the sequence's play range
|
|
TRange<FFrameNumber> PlaybackRange = MovieScene->GetPlaybackRange();
|
|
|
|
const FFrameNumber SrcStartFrame = UE::MovieScene::DiscreteInclusiveLower(PlaybackRange);
|
|
const FFrameNumber SrcEndFrame = UE::MovieScene::DiscreteExclusiveUpper(PlaybackRange);
|
|
|
|
const FFrameTime EndingTime = ConvertFrameTime(SrcEndFrame, TickResolution, DisplayRate);
|
|
|
|
const FFrameNumber StartingFrame = ConvertFrameTime(SrcStartFrame, TickResolution, DisplayRate).FloorToFrame();
|
|
const FFrameNumber EndingFrame = EndingTime.FloorToFrame();
|
|
|
|
SetFrameRange(StartingFrame.Value, (EndingFrame - StartingFrame).Value, EndingTime.GetSubFrame());
|
|
}
|
|
|
|
// Reset the play position based on the user-specified start offset, or a random time
|
|
FFrameTime SpecifiedStartOffset = PlaybackSettings.StartTime * DisplayRate;
|
|
|
|
// Setup the starting time
|
|
FFrameTime StartingTimeOffset = PlaybackSettings.bRandomStartTime
|
|
? FFrameTime(FMath::Rand() % GetFrameDuration())
|
|
: FMath::Clamp<FFrameTime>(SpecifiedStartOffset, 0, GetFrameDuration()-1);
|
|
|
|
StartTimeWithOffset = StartTime + StartingTimeOffset;
|
|
|
|
ClockToUse = MovieScene->GetClockSource();
|
|
|
|
if (ClockToUse == EUpdateClockSource::Custom)
|
|
{
|
|
TimeController = MovieScene->MakeCustomTimeController(GetPlaybackContext());
|
|
}
|
|
}
|
|
|
|
if (!TimeController.IsValid())
|
|
{
|
|
switch (ClockToUse)
|
|
{
|
|
case EUpdateClockSource::Audio: TimeController = MakeShared<FMovieSceneTimeController_AudioClock>(); break;
|
|
case EUpdateClockSource::Platform: TimeController = MakeShared<FMovieSceneTimeController_PlatformClock>(); break;
|
|
case EUpdateClockSource::RelativeTimecode: TimeController = MakeShared<FMovieSceneTimeController_RelativeTimecodeClock>(); break;
|
|
case EUpdateClockSource::Timecode: TimeController = MakeShared<FMovieSceneTimeController_TimecodeClock>(); break;
|
|
case EUpdateClockSource::PlayEveryFrame: TimeController = MakeShared<FMovieSceneTimeController_PlayEveryFrame>(); break;
|
|
default: TimeController = MakeShared<FMovieSceneTimeController_Tick>(); break;
|
|
}
|
|
|
|
if (!ensureMsgf(TimeController.IsValid(), TEXT("No time controller specified for sequence playback. Falling back to Engine Tick clock source.")))
|
|
{
|
|
TimeController = MakeShared<FMovieSceneTimeController_Tick>();
|
|
}
|
|
}
|
|
|
|
if (!TickManager)
|
|
{
|
|
InitializeForTick(GetPlaybackContext());
|
|
}
|
|
|
|
FMovieSceneSequenceTickInterval TickInterval = PlaybackSettings.bInheritTickIntervalFromOwner
|
|
? FMovieSceneSequenceTickInterval::GetInheritedInterval(this)
|
|
: PlaybackSettings.TickInterval;
|
|
|
|
// If we haven't registered with the tick manager yet, register directly
|
|
if (!RegisteredTickInterval.IsSet())
|
|
{
|
|
TickManager->RegisterTickClient(TickInterval, this);
|
|
}
|
|
// If we were already registered with a different Tick Interval we need to re-register with the new one, which involves tearing everything down and setting up a new instance
|
|
else if (RegisteredTickInterval.GetValue() != TickInterval)
|
|
{
|
|
RootTemplateInstance.TearDown();
|
|
TickManager->UnregisterTickClient(this);
|
|
|
|
TickManager->RegisterTickClient(RegisteredTickInterval.GetValue(), this);
|
|
}
|
|
|
|
RegisteredTickInterval = TickInterval;
|
|
|
|
RootTemplateInstance.Initialize(*Sequence, *this, nullptr);
|
|
|
|
if (!PlaybackSettings.bDynamicWeighting)
|
|
{
|
|
UMovieSceneCompiledDataManager* CompiledDataManager = RootTemplateInstance.GetCompiledDataManager();
|
|
FMovieSceneCompiledDataID CompiledDataID = RootTemplateInstance.GetCompiledDataID();
|
|
if (CompiledDataManager && CompiledDataID.IsValid())
|
|
{
|
|
PlaybackSettings.bDynamicWeighting = EnumHasAnyFlags(CompiledDataManager->GetEntryRef(CompiledDataID).AccumulatedFlags, EMovieSceneSequenceFlags::DynamicWeighting);
|
|
}
|
|
}
|
|
|
|
LatentActionManager.ClearLatentActions();
|
|
|
|
// Set up playback position (with offset) after Stop(), which will reset the starting time to StartTime
|
|
PlayPosition.Reset(StartTimeWithOffset);
|
|
TimeController->Reset(GetCurrentTime());
|
|
|
|
// Update the sync properties on the server.
|
|
UpdateNetworkSyncProperties();
|
|
// On the client, we also update LastKnownPosition. This is because our first PostNetReceive
|
|
// could be called with an incomplete set of replicated values in very rare cases... so for instance we might
|
|
// get the proper LastKnownStatus from the server, but, say, not the proper LastKnownPosition. If the sequence does
|
|
// not start at frame 0, we would see LastKnownPosition left at 0, while our own client-side position is the first
|
|
// frame of the sequence, as initialized above (SetFrameRange). At this point, we would incorrectly assume that the server
|
|
// jumped to frame 0 and we would do the same, when really the server hasn't moved and it's just that the correct
|
|
// LastKnownPosition value is coming in a later net packet.
|
|
NetSyncProps.LastKnownPosition = PlayPosition.GetCurrentPosition();
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::Update(const float DeltaSeconds)
|
|
{
|
|
UWorld* World = GetPlaybackWorld();
|
|
float CurrentWorldTime = 0.f;
|
|
if (World)
|
|
{
|
|
CurrentWorldTime = World->GetTimeSeconds();
|
|
}
|
|
|
|
UpdateNetworkSync();
|
|
|
|
if (TimeControllerState == ETimeControllerState::PreparingToPlay && TimeController->IsReadyToPlay())
|
|
{
|
|
StartTimeControllerAndBroadcastPlayState();
|
|
TimeControllerState = ETimeControllerState::ReadyToPlay;
|
|
}
|
|
|
|
if (TimeControllerState == ETimeControllerState::ReadyToPlay && IsPlaying())
|
|
{
|
|
// Delta seconds has already been multiplied by GetEffectiveTimeDilation at this point, so don't pass that through to Tick
|
|
float PlayRate = bReversePlayback ? -PlaybackSettings.PlayRate : PlaybackSettings.PlayRate;
|
|
|
|
float DeltaTimeForFunction = DeltaSeconds;
|
|
|
|
TimeController->Tick(DeltaTimeForFunction, PlayRate);
|
|
|
|
if (World)
|
|
{
|
|
PlayRate *= World->GetWorldSettings()->GetEffectiveTimeDilation();
|
|
}
|
|
|
|
if (!bSkipNextUpdate)
|
|
{
|
|
check(!IsEvaluating());
|
|
|
|
FFrameTime NewTime = TimeController->RequestCurrentTime(GetCurrentTime(), PlayRate, GetDisplayRate());
|
|
UpdateTimeCursorPosition(NewTime, EUpdatePositionMethod::Play);
|
|
}
|
|
|
|
bSkipNextUpdate = false;
|
|
|
|
// CAREFUL with stateful changes after this... in 95% of cases, the sequence evaluation was
|
|
// only queued up, and hasn't run yet!
|
|
}
|
|
|
|
if (World)
|
|
{
|
|
LastTickGameTimeSeconds = CurrentWorldTime;
|
|
}
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::TickFromSequenceTickManager(float DeltaSeconds, FMovieSceneEntitySystemRunner* InRunner)
|
|
{
|
|
TGuardValue<FMovieSceneEntitySystemRunner*> RunnerGuard(CurrentRunner, InRunner);
|
|
UpdateAsync(DeltaSeconds);
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::UpdateAsync(const float DeltaSeconds)
|
|
{
|
|
check(!bIsAsyncUpdate);
|
|
bIsAsyncUpdate = true;
|
|
|
|
Update(DeltaSeconds);
|
|
|
|
bIsAsyncUpdate = false;
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::UpdateTimeCursorPosition(FFrameTime NewPosition, EUpdatePositionMethod Method, bool bHasJumpedOverride)
|
|
{
|
|
if (ensure(!IsEvaluating()))
|
|
{
|
|
UpdateTimeCursorPosition_Internal(NewPosition, Method, bHasJumpedOverride);
|
|
}
|
|
}
|
|
|
|
EMovieScenePlayerStatus::Type UpdateMethodToStatus(EUpdatePositionMethod Method)
|
|
{
|
|
switch(Method)
|
|
{
|
|
case EUpdatePositionMethod::Scrub: return EMovieScenePlayerStatus::Scrubbing;
|
|
case EUpdatePositionMethod::Jump: return EMovieScenePlayerStatus::Stopped;
|
|
case EUpdatePositionMethod::Play: return EMovieScenePlayerStatus::Playing;
|
|
default: return EMovieScenePlayerStatus::Stopped;
|
|
}
|
|
}
|
|
|
|
FMovieSceneEvaluationRange UpdatePlayPosition(FMovieScenePlaybackPosition& InOutPlayPosition, FFrameTime NewTime, EUpdatePositionMethod Method, bool bPreferReversePlayback)
|
|
{
|
|
EPlayDirection PreferredDirection = bPreferReversePlayback ? EPlayDirection::Backwards : EPlayDirection::Forwards;
|
|
if (Method == EUpdatePositionMethod::Play)
|
|
{
|
|
return InOutPlayPosition.PlayTo(NewTime, PreferredDirection);
|
|
}
|
|
|
|
return InOutPlayPosition.JumpTo(NewTime, PreferredDirection);
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::StartTimeControllerAndBroadcastPlayState()
|
|
{
|
|
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 (GSequencerApplyDisplayRateToDynResFrameTimeBudget)
|
|
{
|
|
const float DyResFrameTimeBudget = PlayPosition.GetInputRate().AsInterval() * 1000.0f;
|
|
|
|
static IConsoleVariable* CVarDynResFrameTimeBudget = IConsoleManager::Get().FindConsoleVariable(TEXT("r.DynamicRes.FrameTimeBudget"));
|
|
CVarDynResFrameTimeBudget->Set(DyResFrameTimeBudget, ECVF_SetByCode);
|
|
|
|
bOverridingDynResFrameTimeBudget = true;
|
|
}
|
|
}
|
|
|
|
if (!PlayPosition.GetLastPlayEvalPostition().IsSet() || PlayPosition.GetLastPlayEvalPostition() != PlayPosition.GetCurrentPosition())
|
|
{
|
|
EPlayDirection PreferredDirection = bReversePlayback ? EPlayDirection::Backwards : EPlayDirection::Forwards;
|
|
UpdateMovieSceneInstance(PlayPosition.PlayTo(PlayPosition.GetCurrentPosition(), PreferredDirection), EMovieScenePlayerStatus::Playing);
|
|
}
|
|
|
|
RunLatentActions();
|
|
UpdateNetworkSyncProperties();
|
|
|
|
if (bReversePlayback)
|
|
{
|
|
if (OnPlayReverse.IsBound())
|
|
{
|
|
OnPlayReverse.Broadcast();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (OnPlay.IsBound())
|
|
{
|
|
OnPlay.Broadcast();
|
|
}
|
|
}
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::UpdateTimeCursorPosition_Internal(FFrameTime NewPosition, EUpdatePositionMethod Method, bool bHasJumpedOverride)
|
|
{
|
|
EMovieScenePlayerStatus::Type StatusOverride = UpdateMethodToStatus(Method);
|
|
|
|
const int32 Duration = DurationFrames;
|
|
if (Duration == 0 && DurationSubFrames == 0.f)
|
|
{
|
|
if (bWarnZeroDuration)
|
|
{
|
|
bWarnZeroDuration = false;
|
|
const FString SequenceName = GetSequenceName(true);
|
|
UE_LOG(LogMovieScene, Warning, TEXT("Attempting to play back sequence %s with zero duration"), *SequenceName);
|
|
}
|
|
return;
|
|
}
|
|
bWarnZeroDuration = true;
|
|
|
|
if (bPendingOnStartedPlaying)
|
|
{
|
|
OnStartedPlaying();
|
|
bPendingOnStartedPlaying = false;
|
|
}
|
|
|
|
if (Method == EUpdatePositionMethod::Play)
|
|
{
|
|
const FMovieSceneSequenceHierarchy* Hierarchy = RootTemplateInstance.GetHierarchy();
|
|
|
|
if (Hierarchy && Hierarchy->GetRootTransform().FindFirstWarpDomain() == UE::MovieScene::ETimeWarpChannelDomain::PlayRate)
|
|
{
|
|
NewPosition = ConvertFrameTime(NewPosition, PlayPosition.GetInputRate(), PlayPosition.GetOutputRate());
|
|
NewPosition = Hierarchy->GetRootTransform().TransformTime(NewPosition);
|
|
NewPosition = ConvertFrameTime(NewPosition, PlayPosition.GetOutputRate(), PlayPosition.GetInputRate());
|
|
}
|
|
}
|
|
|
|
// If we should pause during this evaluation, we'll handle that below.
|
|
const TOptional<TRange<FFrameTime>> PauseRange = GetPauseRange(NewPosition);
|
|
if (!PauseRange.IsSet() && Method == EUpdatePositionMethod::Play && ShouldStopOrLoop(NewPosition))
|
|
{
|
|
// The actual start time taking into account reverse playback
|
|
FFrameTime StartTimeWithReversed = bReversePlayback ? GetLastValidTime() : StartTime;
|
|
|
|
// The actual end time taking into account reverse playback
|
|
FFrameTime EndTimeWithReversed = bReversePlayback ? StartTime : GetLastValidTime();
|
|
|
|
// Operate in tick resolution (for subframes)
|
|
const double DurationWithSubFrames = FMath::Max<double>(UE_SMALL_NUMBER, GetDuration().Time.AsDecimal());
|
|
const double PositionRelativeToStart = (NewPosition - StartTimeWithReversed).AsDecimal();
|
|
|
|
const int32 NumTimesLooped = FMath::Abs(FMath::TruncToInt32(PositionRelativeToStart / DurationWithSubFrames));
|
|
const bool bLoopIndefinitely = PlaybackSettings.LoopCount.Value < 0;
|
|
|
|
// loop playback
|
|
if (bLoopIndefinitely || CurrentNumLoops + NumTimesLooped <= PlaybackSettings.LoopCount.Value)
|
|
{
|
|
CurrentNumLoops += NumTimesLooped;
|
|
if (NumTimesLooped > 0)
|
|
{
|
|
// Reset server time samples when this player has looped. This ensures that
|
|
// smoothed playback (if enabled) does not result in a smoothed frame in the previous
|
|
// loop.
|
|
ServerTimeSamples.Reset();
|
|
}
|
|
|
|
// Finish evaluating any frames left in the current loop in case they have events attached
|
|
FFrameTime CurrentPosition = PlayPosition.GetCurrentPosition();
|
|
if ((bReversePlayback && CurrentPosition > EndTimeWithReversed) ||
|
|
(!bReversePlayback && CurrentPosition < EndTimeWithReversed))
|
|
{
|
|
FMovieSceneEvaluationRange Range = PlayPosition.PlayTo(EndTimeWithReversed);
|
|
UpdateMovieSceneInstance(Range, StatusOverride);
|
|
}
|
|
|
|
const FFrameTime Overplay = FFrameTime::FromDecimal(FMath::Fmod(PositionRelativeToStart, DurationWithSubFrames));
|
|
FFrameTime NewFrameOffset;
|
|
|
|
if (bReversePlayback)
|
|
{
|
|
NewFrameOffset = (Overplay > 0) ? FFrameTime(Duration) + Overplay : Overplay;
|
|
}
|
|
else
|
|
{
|
|
NewFrameOffset = (Overplay < 0) ? FFrameTime(Duration) + Overplay : Overplay;
|
|
}
|
|
|
|
if (SpawnRegister.IsValid())
|
|
{
|
|
SpawnRegister->ForgetExternallyOwnedSpawnedObjects(GetSharedPlaybackState());
|
|
}
|
|
|
|
// Reset the play position, and generate a new range that gets us to the new frame time
|
|
if (bReversePlayback)
|
|
{
|
|
PlayPosition.Reset(Overplay > 0 ? GetLastValidTime() : StartTimeWithReversed);
|
|
}
|
|
else
|
|
{
|
|
PlayPosition.Reset(Overplay < 0 ? GetLastValidTime() : StartTimeWithReversed);
|
|
}
|
|
|
|
FMovieSceneEvaluationRange Range = PlayPosition.PlayTo(StartTimeWithReversed + NewFrameOffset);
|
|
|
|
const bool bHasJumped = true;
|
|
UpdateMovieSceneInstance(Range, StatusOverride, bHasJumped);
|
|
|
|
// Use the exact time here rather than a frame locked time to ensure we don't skip the amount that was overplayed in the time controller
|
|
FQualifiedFrameTime ExactCurrentTime(StartTimeWithReversed + NewFrameOffset, PlayPosition.GetInputRate());
|
|
TimeController->Reset(ExactCurrentTime);
|
|
|
|
UpdateNetworkSyncProperties();
|
|
|
|
OnLooped();
|
|
}
|
|
|
|
// We reached the end of playback
|
|
else
|
|
{
|
|
// Clamp the position to the duration
|
|
NewPosition = FMath::Clamp(NewPosition, FFrameTime(StartTime), GetLastValidTime());
|
|
|
|
FMovieSceneEvaluationRange Range = UpdatePlayPosition(PlayPosition, NewPosition, Method, bReversePlayback);
|
|
UpdateMovieSceneInstance(Range, StatusOverride);
|
|
|
|
// We have authority to finish playback if:
|
|
// 1. There's no playback replication (standalone sequence)
|
|
// 2. We are the server side of a replicated sequence
|
|
// 3. We are the client side of a replicated sequence, but playing is only happening on our side (i.e. the Play() method was
|
|
// called only on the client, and the server sequence is stopped)
|
|
const bool bHasAuthorityToFinish = (
|
|
(!PlaybackClient || !PlaybackClient->GetIsReplicatedPlayback()) ||
|
|
HasAuthority() ||
|
|
NetSyncProps.LastKnownStatus == EMovieScenePlayerStatus::Stopped);
|
|
const FString SequenceName = GetSequenceName(true);
|
|
if (bHasAuthorityToFinish)
|
|
{
|
|
UE_LOG(LogMovieScene, Verbose,
|
|
TEXT("Finishing sequence %s at frame %s since we have authority."),
|
|
*SequenceName, *LexToString(NewPosition));
|
|
FinishPlaybackInternal(NewPosition);
|
|
|
|
// Explicitly tell the clients to finish their playback. They won't have called FinishPlaybackInternal
|
|
// because it's in the line right above, only for sequence players with some authority
|
|
// (client only or server).
|
|
RPC_OnFinishPlaybackEvent(NewPosition, SerialNumber + 1);
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogMovieScene, Verbose,
|
|
TEXT("Keeping sequence %s at frame %s while waiting for playback finish from server."),
|
|
*SequenceName, *LexToString(NewPosition));
|
|
}
|
|
|
|
UpdateNetworkSyncProperties();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// If the desired evaluation will take us past where we want to go we need to use a clipped range provided by the PauseRange, otherwise use the normal one.
|
|
FMovieSceneEvaluationRange Range = PauseRange.IsSet()
|
|
? FMovieSceneEvaluationRange(PauseRange.GetValue(), PlayPosition.GetOutputRate(), bReversePlayback ? EPlayDirection::Backwards : EPlayDirection::Forwards)
|
|
: UpdatePlayPosition(PlayPosition, NewPosition, Method, bReversePlayback);
|
|
|
|
UMovieSceneSequence* MovieSceneSequence = RootTemplateInstance.GetSequence(MovieSceneSequenceID::Root);
|
|
const bool bIsSequenceBlocking = MovieSceneSequence ? EnumHasAnyFlags(MovieSceneSequence->GetFlags(), EMovieSceneSequenceFlags::BlockingEvaluation) : false;
|
|
|
|
// Just update the time and sequence... if we are in the main level update we want, if possible,
|
|
// to only queue this sequence's update, so everything updates in parallel. If not possible, or if
|
|
// not in the main level update, we run the evaluation synchronously.
|
|
FMovieSceneUpdateArgs Args;
|
|
Args.bIsAsync = (bIsAsyncUpdate && !bIsSequenceBlocking);
|
|
Args.bHasJumped = bHasJumpedOverride;
|
|
|
|
PostEvaluationCallbacks.Add(FOnEvaluationCallback::CreateUObject(this, &UMovieSceneSequencePlayer::UpdateNetworkSyncProperties));
|
|
|
|
UpdateMovieSceneInstance(Range, StatusOverride, Args);
|
|
|
|
// Now that the evaluation has taken place we call Pause (to trigger delegates, etc.), however if we're running the evaluation
|
|
// as async we actually need to queue a latent action to do it so that it happens after the data is actually updated. We can't
|
|
// use the QueueLatentAction inside of Pause() because IsEvaluating() is only set during actual evaluation so it won't be set yet.
|
|
if (PauseRange.IsSet())
|
|
{
|
|
if (Args.bIsAsync)
|
|
{
|
|
QueueLatentAction(FMovieSceneSequenceLatentActionDelegate::CreateUObject(this, &UMovieSceneSequencePlayer::Pause));
|
|
}
|
|
else
|
|
{
|
|
Pause();
|
|
}
|
|
}
|
|
}
|
|
|
|
// WARNING: DO NOT CHANGE PLAYER STATE ANYMORE HERE!
|
|
// The code path above (in the "else" statement) queues an asynchronous evaluation, so any further
|
|
// state change must be moved in the first first block, with a post-evaluation callback in the second
|
|
// block... see `UpdateNetworkSyncProperties` as an example.
|
|
}
|
|
|
|
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())
|
|
{
|
|
UE_LOG(LogMovieScene, Error, TEXT("Refusing to update an unobservable sequence! Did it become unobservable during playback?"));
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::TearDown()
|
|
{
|
|
RootTemplateInstance.TearDown();
|
|
|
|
if (TickManager)
|
|
{
|
|
RegisteredTickInterval.Reset();
|
|
TickManager->UnregisterTickClient(this);
|
|
TickManager = nullptr;
|
|
}
|
|
|
|
Status = EMovieScenePlayerStatus::Stopped;
|
|
}
|
|
|
|
bool UMovieSceneSequencePlayer::IsValid() const
|
|
{
|
|
return RootTemplateInstance.IsValid();
|
|
}
|
|
|
|
bool UMovieSceneSequencePlayer::HasDynamicWeighting() const
|
|
{
|
|
return PlaybackSettings.bDynamicWeighting;
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::PreEvaluation(const FMovieSceneContext& Context)
|
|
{
|
|
RunPreEvaluationCallbacks();
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::PostEvaluation(const FMovieSceneContext& Context)
|
|
{
|
|
FFrameTime CurrentTime = ConvertFrameTime(Context.GetTime(), Context.GetFrameRate(), PlayPosition.GetInputRate());
|
|
FFrameTime PreviousTime = ConvertFrameTime(Context.GetPreviousTime(), Context.GetFrameRate(), PlayPosition.GetInputRate());
|
|
OnMovieSceneSequencePlayerUpdate.Broadcast(*this, CurrentTime, PreviousTime);
|
|
|
|
RunPostEvaluationCallbacks();
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::RunPreEvaluationCallbacks()
|
|
{
|
|
for (const FOnEvaluationCallback& Callback : PreEvaluationCallbacks)
|
|
{
|
|
Callback.ExecuteIfBound();
|
|
}
|
|
PreEvaluationCallbacks.Reset();
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::RunPostEvaluationCallbacks()
|
|
{
|
|
for (const FOnEvaluationCallback& Callback : PostEvaluationCallbacks)
|
|
{
|
|
Callback.ExecuteIfBound();
|
|
}
|
|
PostEvaluationCallbacks.Reset();
|
|
}
|
|
|
|
FString UMovieSceneSequencePlayer::GetSequenceName(bool bAddClientInfo) const
|
|
{
|
|
if (Sequence)
|
|
{
|
|
FString SequenceName = Sequence->GetName();
|
|
if (bAddClientInfo)
|
|
{
|
|
AActor* Actor = GetTypedOuter<AActor>();
|
|
if (Actor && Actor->GetWorld() && Actor->GetWorld()->GetNetMode() == NM_Client)
|
|
{
|
|
SequenceName += FString::Printf(TEXT(" (client %d)"), UE::GetPlayInEditorID() - 1);
|
|
}
|
|
}
|
|
return SequenceName;
|
|
}
|
|
else
|
|
{
|
|
return LexToString(NAME_None);
|
|
}
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::SetPlaybackClient(TScriptInterface<IMovieScenePlaybackClient> InPlaybackClient)
|
|
{
|
|
PlaybackClient = InPlaybackClient;
|
|
}
|
|
|
|
TSharedPtr<FMovieSceneTimeController> UMovieSceneSequencePlayer::GetTimeController() const
|
|
{
|
|
return TimeController;
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::SetTimeController(TSharedPtr<FMovieSceneTimeController> InTimeController)
|
|
{
|
|
SetTimeControllerDirectly(InTimeController);
|
|
if (TimeController.IsValid())
|
|
{
|
|
TimeController->Reset(GetCurrentTime());
|
|
}
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::SetTimeControllerDirectly(TSharedPtr<FMovieSceneTimeController> InTimeController)
|
|
{
|
|
TimeController = InTimeController;
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::SetIgnorePlaybackReplication(bool bState)
|
|
{
|
|
bIgnorePlaybackReplication = bState;
|
|
}
|
|
|
|
TArray<UObject*> UMovieSceneSequencePlayer::GetBoundObjects(FMovieSceneObjectBindingID ObjectBinding)
|
|
{
|
|
TArray<UObject*> Objects;
|
|
TSharedRef<const UE::MovieScene::FSharedPlaybackState> SharedPlaybackState = GetSharedPlaybackState();
|
|
|
|
for (TWeakObjectPtr<> WeakObject : ObjectBinding.ResolveBoundObjects(MovieSceneSequenceID::Root, SharedPlaybackState))
|
|
{
|
|
if (UObject* Object = WeakObject.Get())
|
|
{
|
|
Objects.Add(Object);
|
|
}
|
|
}
|
|
return Objects;
|
|
}
|
|
|
|
TArray<FMovieSceneObjectBindingID> UMovieSceneSequencePlayer::GetObjectBindings(UObject* InObject)
|
|
{
|
|
TArray<FMovieSceneObjectBindingID> Bindings;
|
|
PRAGMA_DISABLE_DEPRECATION_WARNINGS
|
|
State.FilterObjectBindings(InObject, GetSharedPlaybackState(), &Bindings);
|
|
PRAGMA_ENABLE_DEPRECATION_WARNINGS
|
|
return Bindings;
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::RequestInvalidateBinding(FMovieSceneObjectBindingID ObjectBinding)
|
|
{
|
|
PRAGMA_DISABLE_DEPRECATION_WARNINGS
|
|
State.Invalidate(ObjectBinding.GetGuid(), ObjectBinding.GetRelativeSequenceID());
|
|
PRAGMA_ENABLE_DEPRECATION_WARNINGS
|
|
}
|
|
|
|
UWorld* UMovieSceneSequencePlayer::GetPlaybackWorld() const
|
|
{
|
|
UObject* PlaybackContext = GetPlaybackContext();
|
|
return PlaybackContext ? PlaybackContext->GetWorld() : nullptr;
|
|
}
|
|
|
|
bool UMovieSceneSequencePlayer::HasAuthority() const
|
|
{
|
|
AActor* Actor = GetTypedOuter<AActor>();
|
|
return Actor && Actor->HasAuthority() && IsValidChecked(this) && !IsUnreachable();
|
|
}
|
|
|
|
FFrameTime UMovieSceneSequencePlayer::UpdateServerTimeSamples()
|
|
{
|
|
// Attempt to estimate the server time based on our samples.
|
|
// We need to reproject the samples to the current wall-clock time, based on when they were taken
|
|
const double CurrentWallClock = FPlatformTime::Seconds();
|
|
const double Lifetime = CurrentWallClock - float(GSequencerMaxSmoothedNetSyncSampleAge) / 1000.f;
|
|
const float PlaybackMultiplier = bReversePlayback ? -PlaybackSettings.PlayRate : PlaybackSettings.PlayRate;
|
|
|
|
float TimeDilation = 1.0f;
|
|
if (const UWorld* World = GetPlaybackWorld())
|
|
{
|
|
if (AWorldSettings* WorldSettings = World->GetWorldSettings())
|
|
{
|
|
TimeDilation = WorldSettings->GetEffectiveTimeDilation();
|
|
}
|
|
}
|
|
|
|
// Cull any old samples that were taken more than GSequencerMaxSmoothedNetSyncSampleAge ms ago by
|
|
// Finding the index of the first sample younger than this time
|
|
const int32 FirstValidSample = Algo::LowerBoundBy(ServerTimeSamples, Lifetime, &FServerTimeSample::ReceivedTime);
|
|
if (FirstValidSample >= ServerTimeSamples.Num())
|
|
{
|
|
// Never found a sample that is recent enough, all samples are too old
|
|
ServerTimeSamples.Reset();
|
|
}
|
|
else if (FirstValidSample > 0)
|
|
{
|
|
// Remove from the front of the array up until the first valid sample
|
|
ServerTimeSamples.RemoveAt(0, FirstValidSample);
|
|
}
|
|
|
|
// If we have too many samples, uniformly cull intermediate samples by compacting them into the MaxNumSamples range
|
|
// Making sure to always keep the most recent sample
|
|
const int32 MaxNumSamples = GSequencerMaxSmoothedNetSyncSampleCount;
|
|
if (ServerTimeSamples.Num() > MaxNumSamples)
|
|
{
|
|
float Step = FMath::Max(ServerTimeSamples.Num() / float(MaxNumSamples), 1.f);
|
|
|
|
for (int Index = 1; Index < MaxNumSamples-1; ++Index)
|
|
{
|
|
const int32 RemappedIndex = ServerTimeSamples.Num() - int(Step*Index) - 1;
|
|
ServerTimeSamples[Index] = ServerTimeSamples[RemappedIndex];
|
|
}
|
|
ServerTimeSamples[MaxNumSamples-1] = ServerTimeSamples.Last();
|
|
ServerTimeSamples.RemoveAt(MaxNumSamples, ServerTimeSamples.Num() - MaxNumSamples, EAllowShrinking::Yes);
|
|
}
|
|
|
|
auto UpdateSamplesForChangedTimeDilation = [&]()
|
|
{
|
|
// Project all server time samples back based on the new play-rate and time dilation so future updates will be accurate
|
|
if (LastEffectiveTimeDilation != TimeDilation)
|
|
{
|
|
for (FServerTimeSample& Sample : ServerTimeSamples)
|
|
{
|
|
const double ThisSample = Sample.ServerTime + (CurrentWallClock - Sample.ReceivedTime) * PlaybackMultiplier * LastEffectiveTimeDilation;
|
|
Sample.ReceivedTime = CurrentWallClock - (ThisSample - Sample.ServerTime) / (PlaybackMultiplier * TimeDilation);
|
|
}
|
|
LastEffectiveTimeDilation = TimeDilation;
|
|
}
|
|
};
|
|
|
|
if (ServerTimeSamples.Num() < 10)
|
|
{
|
|
// Fallback to the current time if there are not enough samples
|
|
UpdateSamplesForChangedTimeDilation();
|
|
return PlayPosition.GetCurrentPosition();
|
|
}
|
|
|
|
// Compute the Standard Deviation so we can understand the variance in the samples
|
|
double MeanTime = 0;
|
|
for (const FServerTimeSample& Sample : ServerTimeSamples)
|
|
{
|
|
const double ThisSample = Sample.ServerTime + (CurrentWallClock - Sample.ReceivedTime) * PlaybackMultiplier * LastEffectiveTimeDilation;
|
|
MeanTime += ThisSample;
|
|
}
|
|
MeanTime = MeanTime / ServerTimeSamples.Num();
|
|
|
|
|
|
double StandardDeviation = 0;
|
|
for (const FServerTimeSample& Sample : ServerTimeSamples)
|
|
{
|
|
const double ThisSample = Sample.ServerTime + (CurrentWallClock - Sample.ReceivedTime) * PlaybackMultiplier * LastEffectiveTimeDilation;
|
|
StandardDeviation += FMath::Square(ThisSample - MeanTime);
|
|
}
|
|
StandardDeviation = StandardDeviation / ServerTimeSamples.Num();
|
|
StandardDeviation = FMath::Sqrt(StandardDeviation);
|
|
|
|
const int32 OriginalNum = ServerTimeSamples.Num();
|
|
|
|
// Possibly need to recompute the mean if we discard any samples
|
|
double NewMeanTime = MeanTime;
|
|
|
|
// If the deviation is greater than our threshold, we start culling samples that lie outside it
|
|
const double DeviationThreshold = ((GSequencerSmoothedNetSyncDeviationThreshold * 0.001f) * PlayPosition.GetInputRate()).AsDecimal();
|
|
if (StandardDeviation > DeviationThreshold)
|
|
{
|
|
// Discard anything outside the standard deviation in the hopes that future samples will converge
|
|
for (int32 SampleIndex = ServerTimeSamples.Num()-1; SampleIndex >= 0; --SampleIndex)
|
|
{
|
|
const double ThisSample = ServerTimeSamples[SampleIndex].ServerTime + (CurrentWallClock - ServerTimeSamples[SampleIndex].ReceivedTime) * PlaybackMultiplier * LastEffectiveTimeDilation;
|
|
if (FMath::Abs(ThisSample - MeanTime) > StandardDeviation)
|
|
{
|
|
ServerTimeSamples.RemoveAt(SampleIndex, EAllowShrinking::No);
|
|
}
|
|
else
|
|
{
|
|
NewMeanTime += ThisSample;
|
|
}
|
|
}
|
|
NewMeanTime = NewMeanTime / ServerTimeSamples.Num();
|
|
}
|
|
|
|
UpdateSamplesForChangedTimeDilation();
|
|
|
|
// If we didn't cull too many samples, we have confidence in the data set
|
|
if (ServerTimeSamples.Num() >= OriginalNum/2)
|
|
{
|
|
return NewMeanTime * PlayPosition.GetInputRate();
|
|
}
|
|
else
|
|
{
|
|
// Not enough confidence in the data
|
|
return PlayPosition.GetCurrentPosition();
|
|
}
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::AdvanceClientSerialNumberTo(int32 NewSerialNumber)
|
|
{
|
|
if (ensureAlwaysMsgf(!HasAuthority(), TEXT("Trying to advance the serial number on a server player!")))
|
|
{
|
|
if (ensureAlwaysMsgf(NewSerialNumber >= SerialNumber, TEXT("Advancing to an older serial number!")))
|
|
{
|
|
SerialNumber = NewSerialNumber;
|
|
}
|
|
}
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::RPC_ExplicitServerUpdateEvent_Implementation(EUpdatePositionMethod EventMethod, FFrameTime MarkerTime, int32 NewSerialNumber)
|
|
{
|
|
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Handle an explicit jump/play/scrub command from the server.
|
|
|
|
if (HasAuthority())
|
|
{
|
|
// Never run network sync operations on authoritative players
|
|
ensure(NewSerialNumber > SerialNumber);
|
|
SerialNumber = NewSerialNumber;
|
|
return;
|
|
}
|
|
|
|
if (!Sequence || bIgnorePlaybackReplication)
|
|
{
|
|
// Never run network sync operations on players that have not been initialized yet
|
|
return;
|
|
}
|
|
|
|
// Explicit RPC call - empty our smoothed server samples
|
|
ServerTimeSamples.Reset();
|
|
|
|
#if !NO_LOGGING
|
|
// Log the sync event if necessary
|
|
if (UE_LOG_ACTIVE(LogMovieScene, Verbose))
|
|
{
|
|
const FFrameTime CurrentTime = PlayPosition.GetCurrentPosition();
|
|
const FString SequenceName = GetSequenceName(true);
|
|
UE_LOG(LogMovieScene, Verbose, TEXT("Explicit update event for sequence %s %s @ %s. Server has moved to %s @ %s."),
|
|
*SequenceName,
|
|
*UEnum::GetValueAsString(Status.GetValue()), *LexToString(CurrentTime),
|
|
*UEnum::GetValueAsString(NetSyncProps.LastKnownStatus.GetValue()), *LexToString(MarkerTime));
|
|
}
|
|
#endif
|
|
|
|
// Update our serial number
|
|
AdvanceClientSerialNumberTo(NewSerialNumber);
|
|
|
|
// Explicitly repeat the authoritative update event on this client.
|
|
|
|
// Note: in the case of PlayToFrame this will not necessarily sweep the exact same range as the server did
|
|
// because this client player is unlikely to be at exactly the same time that the server was at when it performed the operation.
|
|
// This is irrelevant for jumps and scrubs as only the new time is meaningful.
|
|
SetPlaybackPosition(FMovieSceneSequencePlaybackParams(MarkerTime, EventMethod));
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::RPC_OnStopEvent_Implementation(FFrameTime StoppedTime, int32 NewSerialNumber)
|
|
{
|
|
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Handle an explicit Stop command from the server.
|
|
|
|
if (HasAuthority())
|
|
{
|
|
// Never run network sync operations on authoritative players
|
|
ensure(NewSerialNumber > SerialNumber);
|
|
SerialNumber = NewSerialNumber;
|
|
return;
|
|
}
|
|
|
|
if (!Sequence || bIgnorePlaybackReplication)
|
|
{
|
|
// Never run network sync operations on players that have not been initialized yet
|
|
return;
|
|
}
|
|
|
|
// Explicit RPC call - empty our smoothed server samples
|
|
ServerTimeSamples.Reset();
|
|
|
|
#if !NO_LOGGING
|
|
if (UE_LOG_ACTIVE(LogMovieSceneRepl, Verbose))
|
|
{
|
|
const FFrameTime CurrentTime = PlayPosition.GetCurrentPosition();
|
|
const FString SequenceName = GetSequenceName(true);
|
|
UE_LOG(LogMovieSceneRepl, Verbose, TEXT("Explicit Stop() event for sequence %s %s @ frame %d, subframe %f. Server has stopped at frame %d, subframe %f."),
|
|
*SequenceName, *UEnum::GetValueAsString(Status.GetValue()),
|
|
CurrentTime.FrameNumber.Value, CurrentTime.GetSubFrame(),
|
|
StoppedTime.FrameNumber.Value, StoppedTime.GetSubFrame());
|
|
}
|
|
#endif
|
|
|
|
// Update our serial number
|
|
AdvanceClientSerialNumberTo(NewSerialNumber);
|
|
|
|
EUpdatePositionMethod UpdatePositionMethod;
|
|
switch (Status.GetValue())
|
|
{
|
|
case EMovieScenePlayerStatus::Playing:
|
|
UpdatePositionMethod = EUpdatePositionMethod::Play;
|
|
break;
|
|
case EMovieScenePlayerStatus::Scrubbing:
|
|
UpdatePositionMethod = EUpdatePositionMethod::Scrub;
|
|
break;
|
|
default:
|
|
UpdatePositionMethod = EUpdatePositionMethod::Jump;
|
|
break;
|
|
}
|
|
|
|
// Catch up with any loops we are missing compared to the server. This is generally just 0 or 1 loops.
|
|
// When it's 1 loop, it's generally because we are very close to the end, and the server somehow stopped
|
|
// near the beginning of the next loop, so we have just a little bit of catching up to do.
|
|
const int32 LoopOffset = (NetSyncProps.LastKnownNumLoops - CurrentNumLoops);
|
|
const FFrameTime LoopEndTime = (bReversePlayback ? StartTime : GetLastValidTime());
|
|
for (int32 LoopIndex = 0; LoopIndex < LoopOffset; ++LoopIndex)
|
|
{
|
|
SetPlaybackPosition(FMovieSceneSequencePlaybackParams(LoopEndTime, UpdatePositionMethod));
|
|
}
|
|
|
|
// Now do the last bit of catch-up for the current loop.
|
|
if (PlayPosition.GetCurrentPosition() < StoppedTime)
|
|
{
|
|
UE_LOG(LogMovieSceneRepl, Verbose, TEXT("Catching up to explicit stop time %s"), *LexToString(StoppedTime));
|
|
SetPlaybackPosition(FMovieSceneSequencePlaybackParams(StoppedTime, UpdatePositionMethod));
|
|
}
|
|
|
|
StopInternal(StoppedTime);
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::RPC_OnFinishPlaybackEvent_Implementation(FFrameTime StoppedTime, int32 NewSerialNumber)
|
|
{
|
|
if (HasAuthority())
|
|
{
|
|
// Never run network sync operations on authoritative players
|
|
ensure(NewSerialNumber > SerialNumber);
|
|
SerialNumber = NewSerialNumber;
|
|
return;
|
|
}
|
|
|
|
if (!Sequence || bIgnorePlaybackReplication)
|
|
{
|
|
// Never run network sync operations on players that have not been initialized yet
|
|
return;
|
|
}
|
|
|
|
const FString SequenceName = GetSequenceName(true);
|
|
UE_LOG(LogMovieScene, Verbose, TEXT("Received RPC event to finish sequence %s at frame %s."), *SequenceName, *LexToString(StoppedTime));
|
|
|
|
// Update our serial number
|
|
AdvanceClientSerialNumberTo(NewSerialNumber);
|
|
|
|
FinishPlaybackInternal(StoppedTime);
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::PostNetReceive()
|
|
{
|
|
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Handle a passive update of the replicated status and time properties of the player.
|
|
|
|
Super::PostNetReceive();
|
|
|
|
if (!ensure(!HasAuthority()) || !Sequence || bIgnorePlaybackReplication)
|
|
{
|
|
// Never run network sync operations on authoritative players or players that have not been initialized yet
|
|
return;
|
|
}
|
|
|
|
// Very occasionally a stray network update can come late, and we need to discard it. One such situation
|
|
// is when the server invokes an RPC to stop and finish the sequence, but late network updates arrive
|
|
// after that for the last few frames, and the client player ends up restarting the sequence to evaluate
|
|
// these last few frames even though it has already stopped from the RPCs.
|
|
if (NetSyncProps.LastKnownSerialNumber < SerialNumber)
|
|
{
|
|
#if !NO_LOGGING
|
|
const FFrameTime CurrentTime = PlayPosition.GetCurrentPosition();
|
|
const FString SequenceName = GetSequenceName(true);
|
|
UE_LOG(LogMovieSceneRepl, Verbose,
|
|
TEXT("Ignoring network update with old serial (%d < %d) for sequence %s %s @ %s. Server was %s @ %s."),
|
|
NetSyncProps.LastKnownSerialNumber, SerialNumber, *SequenceName,
|
|
*UEnum::GetValueAsString(Status.GetValue()), *LexToString(CurrentTime),
|
|
*UEnum::GetValueAsString(NetSyncProps.LastKnownStatus.GetValue()), *LexToString(NetSyncProps.LastKnownPosition));
|
|
#endif
|
|
return;
|
|
}
|
|
|
|
const bool bHasStartedPlaying = NetSyncProps.LastKnownStatus == EMovieScenePlayerStatus::Playing && Status != EMovieScenePlayerStatus::Playing;
|
|
const bool bHasChangedStatus = NetSyncProps.LastKnownStatus != Status;
|
|
const bool bHasChangedTime = NetSyncProps.LastKnownPosition != PlayPosition.GetCurrentPosition();
|
|
|
|
// We need to take play-rate into account when determining how many frames we can lag behind the server.
|
|
// For instance, if we play 3 times faster than normal (play-rate = 3), we should be able to lag 3 times as
|
|
// many frames behind as normal before we force a re-sync.
|
|
const float PlayRate = PlaybackSettings.PlayRate;
|
|
|
|
float TimeDilation = 1.0f;
|
|
if (const UWorld* World = GetPlaybackWorld())
|
|
{
|
|
if (AWorldSettings* WorldSettings = World->GetWorldSettings())
|
|
{
|
|
TimeDilation = WorldSettings->GetEffectiveTimeDilation();
|
|
}
|
|
}
|
|
|
|
const float PingMs = GetPing();
|
|
const FFrameTime PingLag = (PingMs/1000.f) * PlayPosition.GetInputRate() * PlayRate * TimeDilation;
|
|
//const FFrameTime LagThreshold = 0.2f * PlayPosition.GetInputRate();
|
|
//const FFrameTime LagDisparity = FMath::Abs(PlayPosition.GetCurrentPosition() - NetSyncProps.LastKnownPosition);
|
|
|
|
const FFrameTime LagThreshold = (GSequencerNetSyncThresholdMS * 0.001f) * PlayPosition.GetInputRate() * PlayRate * TimeDilation;
|
|
|
|
if (!bHasChangedStatus && !bHasChangedTime)
|
|
{
|
|
// Nothing to do
|
|
return;
|
|
}
|
|
|
|
if (Observer && !Observer->CanObserveSequence())
|
|
{
|
|
// We shouldn't do anything.
|
|
#if !NO_LOGGING
|
|
if (UE_LOG_ACTIVE(LogMovieSceneRepl, Verbose))
|
|
{
|
|
const FFrameTime CurrentTime = PlayPosition.GetCurrentPosition();
|
|
const FString SequenceName = GetSequenceName(true);
|
|
UE_LOG(LogMovieSceneRepl, Verbose, TEXT("Ignoring network update for unobservable sequence %s %s @ %s. Server is %s @ %s."),
|
|
*SequenceName,
|
|
*UEnum::GetValueAsString(Status.GetValue()), *LexToString(CurrentTime),
|
|
*UEnum::GetValueAsString(NetSyncProps.LastKnownStatus.GetValue()), *LexToString(NetSyncProps.LastKnownPosition));
|
|
}
|
|
#endif
|
|
return;
|
|
}
|
|
|
|
#if !NO_LOGGING
|
|
if (UE_LOG_ACTIVE(LogMovieSceneRepl, VeryVerbose))
|
|
{
|
|
const FFrameTime CurrentTime = PlayPosition.GetCurrentPosition();
|
|
const FString SequenceName = GetSequenceName(true);
|
|
UE_LOG(LogMovieSceneRepl, VeryVerbose, TEXT("Network sync for sequence %s %s @ %s. Server is %s @ %s."),
|
|
*SequenceName,
|
|
*UEnum::GetValueAsString(Status.GetValue()), *LexToString(CurrentTime),
|
|
*UEnum::GetValueAsString(NetSyncProps.LastKnownStatus.GetValue()), *LexToString(NetSyncProps.LastKnownPosition));
|
|
}
|
|
#endif
|
|
|
|
// Deal with changes of state from stopped <-> playing separately, as they require slightly different considerations
|
|
if (bHasStartedPlaying)
|
|
{
|
|
// Note: when starting playback, we assume that the client and server were at the same time prior to the server initiating playback
|
|
ServerTimeSamples.Reset();
|
|
|
|
// Initiate playback from our current position
|
|
PlayInternal();
|
|
|
|
const FFrameTime LagDisparity = FMath::Abs(PlayPosition.GetCurrentPosition() - (NetSyncProps.LastKnownPosition + PingLag));
|
|
if (LagDisparity > LagThreshold)
|
|
{
|
|
// Synchronize to the server time as best we can if there is a large disparity
|
|
SetPlaybackPosition(FMovieSceneSequencePlaybackParams(NetSyncProps.LastKnownPosition + PingLag, EUpdatePositionMethod::Play));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (bHasChangedTime)
|
|
{
|
|
// Treat all net updates as the main level update - this ensures they get evaluated as part of the
|
|
// main tick manager
|
|
bIsAsyncUpdate = true;
|
|
|
|
// Make sure the client time matches the server according to the client's current status
|
|
if (Status == EMovieScenePlayerStatus::Playing)
|
|
{
|
|
if (bHasChangedStatus)
|
|
{
|
|
// If the status has changed forcibly play to the server position before setting the new status
|
|
SetPlaybackPosition(FMovieSceneSequencePlaybackParams(NetSyncProps.LastKnownPosition + PingLag, EUpdatePositionMethod::Play));
|
|
}
|
|
else
|
|
{
|
|
// Delay net synchronization until next Update call to ensure that we only issue
|
|
// one desync correction per tick.
|
|
bUpdateNetSync = true;
|
|
}
|
|
}
|
|
else if (Status == EMovieScenePlayerStatus::Scrubbing)
|
|
{
|
|
// Scrub to the new position.
|
|
SetPlaybackPosition(FMovieSceneSequencePlaybackParams(NetSyncProps.LastKnownPosition, EUpdatePositionMethod::Scrub));
|
|
}
|
|
else if (Status == EMovieScenePlayerStatus::Stopped)
|
|
{
|
|
// Both client and server are stopped so just update our (client) position to match the server's.
|
|
UpdatePlayPosition(PlayPosition, NetSyncProps.LastKnownPosition, EUpdatePositionMethod::Jump, bReversePlayback);
|
|
TimeController->Reset(GetCurrentTime());
|
|
}
|
|
|
|
bIsAsyncUpdate = false;
|
|
}
|
|
|
|
if (bHasChangedStatus)
|
|
{
|
|
ServerTimeSamples.Reset();
|
|
|
|
switch (NetSyncProps.LastKnownStatus)
|
|
{
|
|
case EMovieScenePlayerStatus::Paused: Pause(); break;
|
|
case EMovieScenePlayerStatus::Playing: Play(); break;
|
|
case EMovieScenePlayerStatus::Scrubbing: Scrub(); break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::UpdateNetworkSync()
|
|
{
|
|
using namespace UE::MovieScene;
|
|
|
|
if (!bUpdateNetSync)
|
|
{
|
|
return;
|
|
}
|
|
bUpdateNetSync = false;
|
|
|
|
// Only process net playback synchronization if we are still Playing.
|
|
if (Status == EMovieScenePlayerStatus::Playing)
|
|
{
|
|
const float PingMs = GetPing();
|
|
|
|
// We need to take play-rate into account when determining how many frames we can lag behind the server.
|
|
// For instance, if we play 3 times faster than normal (play-rate = 3), we should be able to lag 3 times as
|
|
// many frames behind as normal before we force a re-sync.
|
|
const float PlayRate = PlaybackSettings.PlayRate;
|
|
|
|
float TimeDilation = 1.0f;
|
|
if (const UWorld* World = GetPlaybackWorld())
|
|
{
|
|
if (AWorldSettings* WorldSettings = World->GetWorldSettings())
|
|
{
|
|
TimeDilation = WorldSettings->GetEffectiveTimeDilation();
|
|
}
|
|
}
|
|
|
|
const FFrameTime PingLag = (PingMs/1000.f) * PlayPosition.GetInputRate() * PlayRate * TimeDilation;
|
|
const FFrameTime LagThreshold = (GSequencerNetSyncThresholdMS * 0.001f) * PlayPosition.GetInputRate() * PlayRate * TimeDilation;
|
|
|
|
// When the server has looped back to the start but a client is near the end (and is thus about to loop), we don't want to forcibly synchronize the time unless
|
|
// the *real* difference in time is above the threshold. We compute the real-time difference by adding SequenceDuration*LoopCountDifference to the server position:
|
|
// start srv_time clt_time end
|
|
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
|
// | | | |
|
|
//
|
|
// Let NetSyncProps.LastKnownNumLoops = 1, CurrentNumLoops = 0, bReversePlayback = false
|
|
// => LoopOffset = 1
|
|
// OffsetServerTime = srv_time + FrameDuration*LoopOffset = 1 + 20*1 = 21
|
|
// Difference = 21 - 18 = 3 frames
|
|
const int32 LoopOffset = (NetSyncProps.LastKnownNumLoops - CurrentNumLoops) * (bReversePlayback ? -1 : 1);
|
|
const FFrameTime OffsetServerTime = (NetSyncProps.LastKnownPosition + PingLag) + GetFrameDuration() * LoopOffset;
|
|
|
|
if (LoopOffset != 0)
|
|
{
|
|
// If we crossed a loop boundary, reset the samples
|
|
ServerTimeSamples.Reset();
|
|
}
|
|
|
|
const bool bUseSmoothing = GSequencerMaxSmoothedNetSyncSampleAge != 0;
|
|
if (bUseSmoothing)
|
|
{
|
|
ServerTimeSamples.Add(FServerTimeSample{ OffsetServerTime / PlayPosition.GetInputRate(), FPlatformTime::Seconds() });
|
|
}
|
|
|
|
const FFrameTime SmoothedServerTime = bUseSmoothing ? UpdateServerTimeSamples() : OffsetServerTime;
|
|
const FFrameTime Difference = FMath::Abs(PlayPosition.GetCurrentPosition() - SmoothedServerTime);
|
|
|
|
SET_DWORD_STAT(MovieSceneRepl_NumServerSamples, ServerTimeSamples.Num());
|
|
SET_FLOAT_STAT(MovieSceneRepl_SmoothedServerTime, SmoothedServerTime.AsDecimal());
|
|
|
|
if (Difference > LagThreshold + PingLag)
|
|
{
|
|
#if !NO_LOGGING
|
|
if (UE_LOG_ACTIVE(LogMovieSceneRepl, Log))
|
|
{
|
|
const FFrameTime CurrentTime = PlayPosition.GetCurrentPosition();
|
|
const FString SequenceName = GetSequenceName(true);
|
|
UE_LOG(LogMovieSceneRepl, Log, TEXT("Correcting de-synced play position for sequence %s %s @ %s. Server is %s @ %s, (smoothed: %s). Client ping is %.2fms."),
|
|
*SequenceName,
|
|
*UEnum::GetValueAsString(Status.GetValue()), *LexToString(CurrentTime),
|
|
*UEnum::GetValueAsString(NetSyncProps.LastKnownStatus.GetValue()), *LexToString(NetSyncProps.LastKnownPosition),
|
|
*LexToString(SmoothedServerTime), PingMs);
|
|
}
|
|
#endif
|
|
// We're drastically out of sync with the server so we need to forcibly set the time.
|
|
const FFrameTime LastPosition = FFrameRate::TransformTime(
|
|
PlayPosition.GetCurrentPosition(), PlayPosition.GetInputRate(), PlayPosition.GetOutputRate());
|
|
|
|
// Play to the time only if it is further on in the sequence (in our play direction).
|
|
// Otherwise, jump backwards in time (in our play direction).
|
|
const bool bPlayToFrame = bReversePlayback ? SmoothedServerTime < PlayPosition.GetCurrentPosition() : SmoothedServerTime > PlayPosition.GetCurrentPosition();
|
|
if (bPlayToFrame)
|
|
{
|
|
FMovieSceneSequencePlaybackParams Params(SmoothedServerTime, EUpdatePositionMethod::Play);
|
|
// Indicate that the sequence may have jumped a considerable distance.
|
|
// This especially helps the audio track to stay in-sync after a correction
|
|
Params.bHasJumped = true;
|
|
SetPlaybackPosition(Params);
|
|
}
|
|
else
|
|
{
|
|
SetPlaybackPosition(FMovieSceneSequencePlaybackParams(SmoothedServerTime, EUpdatePositionMethod::Jump));
|
|
}
|
|
|
|
// When playing back we skip this sequence's ticked update to avoid queuing 2 updates this frame
|
|
bSkipNextUpdate = true;
|
|
|
|
// Also skip all events up to the last known position, otherwise if we skipped back in time we
|
|
// will re-trigger events again.
|
|
TSharedRef<FSharedPlaybackState> SharedPlaybackState = GetSharedPlaybackState();
|
|
FEventTriggerControlPlaybackCapability& TriggerControlCapability = SharedPlaybackState->SetOrAddCapability<FEventTriggerControlPlaybackCapability>();
|
|
TriggerControlCapability.DisableEventTriggersUntilTime = LastPosition;
|
|
}
|
|
}
|
|
}
|
|
|
|
float UMovieSceneSequencePlayer::GetPing() const
|
|
{
|
|
float PingMs = 0.0f;
|
|
if (const UWorld* PlayWorld = GetPlaybackWorld())
|
|
{
|
|
const UNetDriver* NetDriver = PlayWorld->GetNetDriver();
|
|
if (NetDriver && NetDriver->ServerConnection && NetDriver->ServerConnection->PlayerController && NetDriver->ServerConnection->PlayerController->PlayerState)
|
|
{
|
|
PingMs = NetDriver->ServerConnection->PlayerController->PlayerState->ExactPing * (bReversePlayback ? -1.f : 1.f);
|
|
}
|
|
}
|
|
return PingMs;
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::BeginDestroy()
|
|
{
|
|
RootTemplateInstance.TearDown();
|
|
|
|
TearDown();
|
|
|
|
Super::BeginDestroy();
|
|
}
|
|
|
|
int32 UMovieSceneSequencePlayer::GetFunctionCallspace(UFunction* Function, FFrame* Stack)
|
|
{
|
|
if (HasAnyFlags(RF_ClassDefaultObject))
|
|
{
|
|
// Try to use the same logic as function libraries for static functions, will try to use the global context to check authority only/cosmetic
|
|
return GEngine->GetGlobalFunctionCallspace(Function, this, Stack);
|
|
}
|
|
|
|
check(GetOuter());
|
|
return GetOuter()->GetFunctionCallspace(Function, Stack);
|
|
}
|
|
|
|
bool UMovieSceneSequencePlayer::CallRemoteFunction(UFunction* Function, void* Parameters, FOutParmRec* OutParms, FFrame* Stack)
|
|
{
|
|
check(!HasAnyFlags(RF_ClassDefaultObject));
|
|
|
|
AActor* Actor = GetTypedOuter<AActor>();
|
|
UNetDriver* NetDriver = Actor ? Actor->GetNetDriver() : nullptr;
|
|
if (NetDriver)
|
|
{
|
|
NetDriver->ProcessRemoteFunction(Actor, Function, Parameters, OutParms, Stack, this);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
#if UE_WITH_IRIS
|
|
void UMovieSceneSequencePlayer::RegisterReplicationFragments(UE::Net::FFragmentRegistrationContext& Context, UE::Net::EFragmentRegistrationFlags RegistrationFlags)
|
|
{
|
|
UE::Net::FReplicationFragmentUtil::CreateAndRegisterFragmentsForObject(this, Context, RegistrationFlags);
|
|
}
|
|
#endif
|
|
|
|
bool UMovieSceneSequencePlayer::NeedsQueueLatentAction() const
|
|
{
|
|
return IsEvaluating();
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::QueueLatentAction(FMovieSceneSequenceLatentActionDelegate Delegate)
|
|
{
|
|
if (ensure(TickManager) && !EnumHasAnyFlags(Sequence->GetFlags(), EMovieSceneSequenceFlags::BlockingEvaluation))
|
|
{
|
|
// Queue latent actions on the global tick manager.
|
|
TickManager->AddLatentAction(Delegate);
|
|
}
|
|
else
|
|
{
|
|
// Queue latent actions locally.
|
|
LatentActionManager.AddLatentAction(Delegate);
|
|
}
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::RunLatentActions()
|
|
{
|
|
if (!Sequence)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (SynchronousRunner)
|
|
{
|
|
LatentActionManager.RunLatentActions([this]
|
|
{
|
|
this->SynchronousRunner->Flush();
|
|
});
|
|
}
|
|
else if (ensure(TickManager) && !EnumHasAnyFlags(Sequence->GetFlags(), EMovieSceneSequenceFlags::BlockingEvaluation))
|
|
{
|
|
TickManager->RunLatentActions();
|
|
}
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::SetWeight(double InWeight)
|
|
{
|
|
SetWeight(InWeight, MovieSceneSequenceID::Root);
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::SetWeight(double InWeight, FMovieSceneSequenceID SequenceID)
|
|
{
|
|
UMovieSceneEntitySystemLinker* Linker = RootTemplateInstance.GetEntitySystemLinker();
|
|
if (Linker)
|
|
{
|
|
if (!SequenceWeights)
|
|
{
|
|
SequenceWeights = MakeUnique<UE::MovieScene::FSequenceWeights>(Linker, RootTemplateInstance.GetRootInstanceHandle());
|
|
|
|
if (!PlaybackSettings.bDynamicWeighting && Sequence)
|
|
{
|
|
FText Text = NSLOCTEXT("UMovieSceneSequencePlayer", "SetWeightWarning", "Attempting to set a weight on sequence {0} with PlaybackSettings.bDynamicWeighting disabled. This may lead to undesireable blending artifacts or broken in/out blends.");
|
|
FFrame::KismetExecutionMessage(*FText::Format(Text, FText::FromString(Sequence->GetName())).ToString(), ELogVerbosity::Warning);
|
|
}
|
|
}
|
|
|
|
SequenceWeights->SetWeight(SequenceID, InWeight);
|
|
}
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::RemoveWeight()
|
|
{
|
|
RemoveWeight(MovieSceneSequenceID::Root);
|
|
}
|
|
|
|
void UMovieSceneSequencePlayer::RemoveWeight(FMovieSceneSequenceID SequenceID)
|
|
{
|
|
UMovieSceneEntitySystemLinker* Linker = RootTemplateInstance.GetEntitySystemLinker();
|
|
if (Linker && SequenceWeights)
|
|
{
|
|
SequenceWeights->RemoveWeight(SequenceID);
|
|
}
|
|
}
|