【UE4】Slomo 顿帧

本文以 UE4 自带的 ActionRPG 为例,记录 UE4 的慢动作(顿帧 slomo)的效果及实现,并且记录 slomo
状态下如何调整 Timer 使得计时器时间同样变化

一、Global Slomo

  在启动 UE4 编辑器后,点击 ` 按钮,开启调试命令行,输入 slomo 0.1,即可实现慢动作效果(顿帧效果),慢放速度是 0.1 倍(如下图所示)。

  在动画条上,也可以配置 Anim Notify Slomo 来达到调整世界速度的效果(如下图所示)

  在 ActionRPG 中,将斧头普攻上配置 AnimNotifySlomo,并设置为 0.1 后,可以看到效果如下所示,在进行普攻并触发 Notify 之后,整个世界 的速度都变慢了(火焰、主角自己、怪物)。

  不管是从蓝图,还是从 C++ 设置,最终都是调用 Engine\Classes\Kismet\GameplayStatics.h 中的方法:

	/**
	 * Sets the global time dilation.
	 * @param	TimeDilation	value to set the global time dilation to
	 */
	UFUNCTION(BlueprintCallable, Category="Utilities|Time", meta=(WorldContext="WorldContextObject") )
	static void SetGlobalTimeDilation(const UObject* WorldContextObject, float TimeDilation);

  UE4 管这个设置时间速度的方法叫 SetGlobalTimeDilation,即 设置世界时间膨胀。这个函数内调用 AWorldSettings::SetTimeDilation 达到调整时间速度效果:

float AWorldSettings::SetTimeDilation(float NewTimeDilation)
{
    
    
	TimeDilation = FMath::Clamp(NewTimeDilation, MinGlobalTimeDilation, MaxGlobalTimeDilation);
	return TimeDilation;
}

  这个 TimeDilation调整世界 Tick 的 DeltaSeconds,所以可以让世界变慢。改变 DeltaSeconds 的函数是:

float AWorldSettings::FixupDeltaSeconds(float DeltaSeconds, float RealDeltaSeconds)
{
    
    
	// DeltaSeconds is assumed to be fully dilated at this time, so we will dilate the clamp range as well
	float const Dilation = GetEffectiveTimeDilation();
	float const MinFrameTime = MinUndilatedFrameTime * Dilation;
	float const MaxFrameTime = MaxUndilatedFrameTime * Dilation;

	// clamp frame time according to desired limits
	return FMath::Clamp(DeltaSeconds, MinFrameTime, MaxFrameTime);	
}

二、Custom Slomo

  但是有的时候,我们不想让世界变慢,只想让特点事物变慢,比如只让我自己变慢,或者让我和我打击的目标变慢,那就不能用 Global 的 TimeDilation 来调整,而是每个 Actor 自己的时间。
  UE4 对每个 Actor 都存了一个自己的时间膨胀速度:

/** Allow each actor to run at a different time speed. 
	The DeltaTime for a frame is multiplied by the global TimeDilation (in WorldSettings) 
	and this CustomTimeDilation for this actor's tick.  */
	UPROPERTY(BlueprintReadWrite, AdvancedDisplay, Category=Actor)
	float CustomTimeDilation;

  也就是说对于每个Actor,它的时间膨胀速度是由 Global Dilation 乘上自己的 Custom Dilation 得到的。
  如果只想让某个 Actor 变慢的话,Global 的 Time Dilation 不变,调整每个 Actor 自己的 CustomTimeDilation 可以达到同样的效果,如下所示(只有主角自己"变慢了",火焰和怪物都是正常速度):

三、Slomo 对 Timer 的影响

  但是 UE4 里的计时器,即 TimerManager 里的 SetTimer 设置定时多久后触发的函数,是按照时间的 DeltaSeconds 来计算的,也正常情况下,SetTimer 3 秒后触发的 Delegate,会在从开始计时起,World 的 Tick 里的 DeltaTime 累计超过 3 的时候触发。
  如果设置了世界慢放,即调整了 World 的 TimeDilation,那么 World 的 Tick 的 DeltaTime 每次就变少了,比如正常是 0.01667(60帧每秒),那么 slomo 0.1 之后的 DeltaTime 就是 0.001667。
  所以用 World 里(GameInstance里)的 TimerManager 设置的 Timer,就会按照世界的速度来计算定时。但是如果只改变了自己的 CustomTimeDilation,World 里的TimerManager 设置的 Timer 还是按照正常的速度进行 TIck,就会比角色快很多。
  解决这个问题有两种方法:

  1. 改变 CustomTimeDilation 时候重新绑定 Timer
  2. Actor 身上创建一个自己的 TimerManager

3.1 改变 CustomTimeDilation 时候重新绑定 Timer

  比如在 SetTimer 时候这样调用:

GetWorld()->GetTimerManager().SetTimer(MyTimerHandle, FTimerDelegate::CreateUObject(this, &AMyPlayer::DoPrint, "abc"), 3.0, false);

  那么在 Slomo 改变的 CallBack 里可以调用下边函数(传入设置的 SlomoValue)重新绑定:

const float TimeRemain = GetWorld()->GetTimerManager().GetTimerRemaining(ResetNextAbilityTimerHandle);
if (TimeRemain > 0.f)
{
    
    
	GetWorld()->GetTimerManager().SetTimer(MyTimerHandle, FTimerDelegate::CreateUObject(this, &AMyPlayer::DoPrint, "abc"), TimeRemain / SlomoValue, false);
}

  即,如果这个 Timer 还剩 1.3 秒结束并触发绑定的 Delegate,那么重新绑定这个 Timer,事件是 1.3 除以 Slomo 的值,比如 slomo 0.1,那么就是 1.3 / 0.1 = 13,即 13 秒后触发。
SetTimer 的注释里有写道:

/**
 * Sets a timer to call the given native function at a set interval.
 * If a timer is already set for this handle, it will replace the current timer.
 */

3.2 Actor 身上创建一个自己的 TimerManager

  但是上述方法需要把每个绑定的事件都这么触发,且之后每次新添加 Timer 都需要考虑这个问题,所以很麻烦。
  因为 UE4 的整个世界可以认为都是建立在 Tick 上的,所以在 Actor 身上添加一个自己的 TimerManager 然后在自己的 Tick 里调用 TimerManager 的 Tick 即可。(这一步很重要,因为 TimerManager 不会自己计算时间,World 的 Global TimerManager 也是在 Engine 的 Tick 里做的自己的 Tick,并且用世界的 DeltaSeconds,作为自己的 DeltaSeconds):

void UEditorEngine::Tick( float DeltaSeconds, bool bIdleMode )
{
    
    
	// ...
	
	// Update the timer manager
	TimerManager->Tick(DeltaSeconds);
	
	// ...
}

  在自己的 Character 的构造函数中创建一个 TimerManager:

AMyCharacter::AMyCharacter(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
    
    
	// ...
	CharacterTimerManager = MakeUnique<FTimerManager>();
}

  就可以直接在自己的 Character 的 Tick 里 Tick 自己的 TimerManager了:

// 头文件中
inline FTimerManager& GetCharacterTimerManager() const {
    
     return *CharacterTimerManager.Get(); }
void AMyCharacter::Tick(float DeltaTime)
{
    
    
	Super::Tick(DeltaTime);

	GetCharacterTimerManager().Tick(DeltaTime);
}

  原来的 GetWorld()->GetTimerManager().SetTimer(...); 直接改成 GetCharacterTimerManager().SetTimer(...); 就可以了。这样自己身上的 Timer 会根据自己的 slomo 来计算事时间,该快快,该慢慢。

猜你喜欢

转载自blog.csdn.net/Bob__yuan/article/details/109689234
UE4