LevelSequence源碼分析

前言

這篇文章主要講的是Unreal LevelSequence RunTime的部分。即在遊戲中運行Level Sequence的源碼解析。(而且拋去Replicated 的Sequence,一般Sequence不會在DS上播,因為比較浪費性能,在DS上播的很少這麼使用,所以本篇自動忽略。)
即,本篇主要講的是單純的只在客戶端運行時的LevelSequence的步驟。

作用

  • 我是如何分析LevelSequence 源碼過程
  • 本篇文章主要講述LevelSequeence中綁定的Actor是如何在運行遊戲時候被運行。
  • 可以解決LevelSequence運行時的相關bug。比如樓主接觸LevelSequence遇到的一個bug,就是Editor Play運行正常,但是在Shipping(正式發佈)版運行,某個被綁定在Sequence中的Actor跟沒綁定一樣。不起作用…

問題分析

我比較喜歡直接講述實際的案例,我們就拿一個例子來說吧,就是Sequence中我們可以很簡單的控制Actor的隱藏,那麼在遊戲中運行時,是如何被隱藏的,隱藏步驟是啥樣的,這個怎麼找納?下面就來說說具體步驟。

1.Editor Sequence中先將Actor 隱藏

  • 從以下,我們知道是通過ActorHiddenInGame實現的

節點

2.堆棧尋找

  • 從上述步驟我們知道隱藏一個Actor,Sequence也是通過ActorHiddenInGame來實現的,於是就知道了
UFUNCTION(BlueprintCallable, Category="Rendering", meta=( DisplayName = "Set Actor Hidden In Game", Keywords = "Visible Hidden Show Hide" ))
virtual void SetActorHiddenInGame(bool bNewHidden);

我們是否可以直接在這個方法里直接斷點一下,尋找到Sequence在Runtime將Actor隱藏的堆棧。這個辦法分析源碼必備之技巧。尤其對於這種一開始摸不着頭腦,可以反向推理。
節點

3.核心知識

我們知道Sequence的類型是:ALevelSequenceActor*
我們知道Sequence在Runtime怎麼播放,是通過下述代碼:
    ALevelSequenceActor::InitializePlayer()
	ALevelSequenceActor->SequencePlayer->Play();
    ALevelSequenceActor->SequencePlayer->Update(DeltaSeconds);

顯而易見,需要知道這個SequencePlayer,它的類型是:ULevelSequencePlayer*
那麼需要了解這兩個類之間的關係即可。很顯然,ULevelSequencePlayer是控制ALevelSequenceActor管理播放的,比如快進,快退,都是通過ULevelSequencePlayer.

1.FMovieSceneEvaluationRange
時間驅動結構,我們知道動畫的運動肯定是基於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

  • 上一幀的時間點
  • 下一幀的時間點
  • 當前的EPlayDirection:Forwards, Backwards
  • 當前速率
2.FMovieSceneContext
FMovieSceneContext(FMovieSceneEvaluationRange InRange)
		: FMovieSceneEvaluationRange(InRange)
		, Status(EMovieScenePlayerStatus::Stopped)
	...

對上述FMovieSceneEvaluationRange,再次的封裝,傳到

FMovieSceneRootEvaluationTemplateInstance::Evaluate(FMovieSceneContext Context, IMovieScenePlayer& Player)
就是將IMovieScenePlayer數據 和 記錄的時間結構體FMovieSceneContext,傳給MovieSceneTootEvaluationTemplateInstance中。
3.FMovieSceneRootEvaluationTemplateInstance

節點

FMovieSceneEvaluationTrack 這個數據Info是重點,就是對應到Sequence每一條軌道。

4.FMovieSceneEvaluationGroup

節點
上述堆棧就是找出當前所需要運行的Track List.

5.FMovieSceneExecutionTokens

節點
這就是對實際的需要Track List進行運行。
比如,我一開始遇到的bug:在Editor運行某軌道我想隱藏某Actor是正常的,但是在Shipping正式包,運行了,某軌道運行沒反應,還是沒有被隱藏。於是就斷點查:
節點
節點
最終是通過在Visibility中的Execute 發現foundboundObject一直找不到,才發現原來是Shipping會將場景中一些static打成一個包,所以通過路徑查找obj一直找不到,static的被優化了。所以解決這個問題直接將static改成moveable即可。大部分項目應該都會有此優化。

3.總結

這種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中判斷。

Tags: