找回密码
 立即注册
查看: 462|回复: 2

【UE4】Slomo 顿帧

[复制链接]
发表于 2022-1-14 07:53 | 显示全部楼层 |阅读模式
本文以 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,就会比角色快很多。
解决这个问题有两种方法:

  • 改变 CustomTimeDilation 时候重新绑定 Timer
  • 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();
}
在角色的 Tick 中进行自己的 TimerManager 的 Tick:
void AMyCharacter::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

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


  • 个人博客:

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2022-1-14 07:59 | 显示全部楼层
你这个 是用的系统自带的ActionRPG,我用他自带的资源实现了 同步 网络化。。。
发表于 2022-1-14 08:01 | 显示全部楼层
可以互相交流学习下~
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-9-22 21:34 , Processed in 0.118951 second(s), 23 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表