這篇文章主要講的是Unreal LevelSequence RunTime的部分。即在遊戲中執行Level Sequence的原始碼解析。(而且拋去Replicated 的Sequence,一般Sequence不會在DS上播,因為比較浪費效能,在DS上播的很少這麼使用,所以本篇自動忽略。)
即,本篇主要講的是單純的只在使用者端執行時的LevelSequence的步驟。
我比較喜歡直接講述實際的案例,我們就拿一個例子來說吧,就是Sequence中我們可以很簡單的控制Actor的隱藏,那麼在遊戲中執行時,是如何被隱藏的,隱藏步驟是啥樣的,這個怎麼找納?下面就來說說具體步驟。
UFUNCTION(BlueprintCallable, Category="Rendering", meta=( DisplayName = "Set Actor Hidden In Game", Keywords = "Visible Hidden Show Hide" ))
virtual void SetActorHiddenInGame(bool bNewHidden);
我們是否可以直接在這個方法裡直接斷點一下,尋找到Sequence在Runtime將Actor隱藏的堆疊。這個辦法分析原始碼必備之技巧。尤其對於這種一開始摸不著頭腦,可以反向推理。
我們知道Sequence的型別是:ALevelSequenceActor*
我們知道Sequence在Runtime怎麼播放,是通過下述程式碼:
ALevelSequenceActor::InitializePlayer()
ALevelSequenceActor->SequencePlayer->Play();
ALevelSequenceActor->SequencePlayer->Update(DeltaSeconds);
顯而易見,需要知道這個SequencePlayer,它的型別是:ULevelSequencePlayer*
那麼需要了解這兩個類之間的關係即可。很顯然,ULevelSequencePlayer是控制ALevelSequenceActor管理播放的,比如快進,快退,都是通過ULevelSequencePlayer.
時間驅動結構,我們知道動畫的運動肯定是基於Tick,那麼是如何將DeltaSeconds,傳遞給SequencePlayer,並且還要支援回放,返回,加速等。所以Sequence這裡將時間封裝了一層。因為考慮到如此多的功能,所以封裝成下述時間,需要了解。
inline FFrameTime ConvertFrameTime(FFrameTime SourceTime, FFrameRate SourceRate, FFrameRate DestinationRate)
{
if (SourceRate == DestinationRate)
{
return SourceTime;
}
//We want NewTime =SourceTime * (DestinationRate/SourceRate);
//And want to limit conversions and keep int precision as much as possible
int64 NewNumerator = static_cast<int64>(DestinationRate.Numerator) * SourceRate.Denominator;
int64 NewDenominator = static_cast<int64>(DestinationRate.Denominator) * SourceRate.Numerator;
double NewNumerator_d = double(NewNumerator);
double NewDenominator_d = double(NewDenominator);
//Now the IntegerPart may have a Float Part, and then the FloatPart may have an IntegerPart,
//So we add the extra Float from the IntegerPart to the FloatPart and then add back any extra Integer to IntegerPart
int64 IntegerPart = ( (int64)(SourceTime.GetFrame().Value) * NewNumerator ) / NewDenominator;
const double IntegerFloatPart = ((double(SourceTime.GetFrame().Value) * NewNumerator) / NewDenominator) - double(IntegerPart);
const double FloatPart = ((SourceTime.GetSubFrame() * NewNumerator_d) / NewDenominator_d) + IntegerFloatPart;
const double FloatPartFloored = FMath::FloorToDouble(FloatPart);
const int64 FloatAsInt = int64(FloatPartFloored);
IntegerPart += FloatAsInt;
double SubFrame = FloatPart - FloatPartFloored;
if (SubFrame > 0)
{
SubFrame = FMath::Min(SubFrame, 0.999999940);
}
//@TODO: FLOATPRECISION: FFrameTime needs a general once over for precision (RE: cast to ctor)
return FFrameTime( (int32)IntegerPart, (float)SubFrame);
}
FMovieSceneEvaluationRange
FMovieSceneContext(FMovieSceneEvaluationRange InRange)
: FMovieSceneEvaluationRange(InRange)
, Status(EMovieScenePlayerStatus::Stopped)
...
對上述FMovieSceneEvaluationRange,再次的封裝,傳到
FMovieSceneRootEvaluationTemplateInstance::Evaluate(FMovieSceneContext Context, IMovieScenePlayer& Player)
就是將IMovieScenePlayer資料 和 記錄的時間結構體FMovieSceneContext,傳給MovieSceneTootEvaluationTemplateInstance中。
FMovieSceneEvaluationTrack 這個資料Info是重點,就是對應到Sequence每一條軌道。。
上述堆疊就是找出當前所需要執行的Track List.
這就是對實際的需要Track List進行執行。
比如,我一開始遇到的bug:在Editor執行某軌道我想隱藏某Actor是正常的,但是在Shipping正式包,執行了,某軌道執行沒反應,還是沒有被隱藏。於是就斷點查:
最終是通過在Visibility中的Execute 發現foundboundObject一直找不到,才發現原來是Shipping會將場景中一些static打成一個包,所以通過路徑查詢obj一直找不到,static的被優化了。所以解決這個問題直接將static改成moveable即可。大部分專案應該都會有此優化。
這種Sequence的原始碼分析,可以採用逆向分析,反向打斷點找出堆疊,去除次要邏輯,某些特別難的邏輯,可以拋去,略過。
LavelSequence的原始碼,主要是FMovieSceneRootEvaluationTemplateInstance::EvaluateGroup 根據當前的時間點,查詢出哪些軌道,然後根據每條軌道,做出具體的分別不同的事件。每條軌道的規則很容易理解。其實就只剩下根據時間點查詢出軌道List,這段程式碼其實實在看不懂,其實也不需要太過於糾結了。
這段很難得程式碼就是下述:
void FMovieSceneRootEvaluationTemplateInstance::EvaluateGroup(const FMovieSceneEvaluationPtrCache& EvaluationPtrCache, const FMovieSceneEvaluationGroup& Group, const FMovieSceneContext& RootContext, IMovieScenePlayer& Player)
{
FPersistentEvaluationData PersistentDataProxy(Player);
FMovieSceneEvaluationOperand Operand;
FMovieSceneContext Context = RootContext;
FMovieSceneContext SubContext = Context;
for (const FMovieSceneEvaluationGroupLUTIndex& Index : Group.LUTIndices)
{
int32 TrackIndex = Index.LUTOffset;
// - Do the above in a lockless manner
for (; TrackIndex < Index.LUTOffset + Index.NumInitPtrs + Index.NumEvalPtrs; ++TrackIndex)
{
//略 ***
Track->Evaluate(
SegmentPtr.SegmentID,
Operand,
SubContext,
PersistentDataProxy,
ExecutionTokens);
}
}
ExecutionTokens.Apply(Context, Player);
}
}
上述程式碼有刪改(是我看不懂的,個人感覺也沒必要非要糾結,知道大概意思即可),只要知道 ExecutionTokens,和後面得 ExecutionTokens.Apply(Context, Player) 即可。
比如還有個問題,有人好奇根據時間點Sequence對應的值都不同,這個在哪判斷納,
void FMovieSceneFloatPropertySectionTemplate::Evaluate(const FMovieSceneEvaluationOperand& Operand, const FMovieSceneContext& Context, const FPersistentEvaluationData& PersistentData, FMovieSceneExecutionTokens& ExecutionTokens) const
{
float Result = 0.f;
// Only evaluate if the curve has any data
if (FloatFunction.Evaluate(Context.GetTime(), Result))
{
// Actuator type ID for this property
FMovieSceneBlendingActuatorID ActuatorTypeID = EnsureActuator<float>(ExecutionTokens.GetBlendingAccumulator());
// Add the blendable to the accumulator
const float Weight = EvaluateEasing(Context.GetTime());
ExecutionTokens.BlendToken(ActuatorTypeID, TBlendableToken<float>(Result, BlendType, Weight));
}
}
這就很簡單了,顯然,在各自的Template中判斷。