Post

Latent Timer 延时定时器

Latent Timer 延时定时器

Latent timer

既然已经有了 TimerManager,为什么还要造轮子

因为, 在用TimerManager做玩法的时候,会有不少问题:

  • SetTimerForNextTick 实际上是当帧触发,而不是下帧

  • 回调顺序与定时器的设置顺序不保证一致

  • 设置定时器时,不能指定 tick group

  • TimerManager::Tick 是硬编码在当帧稍晚的时刻

  • 只支持以秒/时间单位设置延迟,你想不想用帧数来设置延迟?

  • 不支持循环次数

  • 尽管已经对 FTimerUnifiedDelegate 做了内存大小的优化,但FTimerDta 还是很臃肿

那我们能解决所有提到的问题么?

嗐,全解决有点难,但解决90%的问题就是小菜一碟,因为我们有个大救星 — FLatentActionManager

FLatentActionManager 是一个简单又强大的工具,用来每帧更新任意的 FPendingLatentAction 实例

每个 LatentAction 会绑定到一个 UObject, 它会在绑定对象的 tick group 更新 ! 但如果绑定对象的更新被禁用了,它就会在“接近帧末”的时刻更新,就在接近且恰好早于TimerManager::Tick的时刻

FPendingLatentAction 可以被继承,做任意你想要的事情。 例如: FDelayUntilNextTickActionFDelayAction 它们就是蓝图中延迟节点的幕后英雄

我解决上述问题的方法:Rem::Latent::FTimerLatentAction_Delay

  • 提供 Rem::Latent::SetTimerForThisTickRem::Latent::SetTimerForNextTick 以在表达延迟一帧的语义时,提供最大限度的明确和灵活性

  • Latent Action 是按照它们绑定到UObject的顺序进行处理的

  • Latent ActionTick group 可以通过绑定一个在目标分组的对象上来控制。 并且顺理成章的支持指定更新依赖

  • 支持按帧数延迟! 这个功能可能不会出现在 TimerManager。 两个辅助结构体: FTimerParameterHelper_TimeFTimerParameterHelper_Frame, 一个统一的API: Rem::Latent::SetTimer

  • 支持指定循环次数 : FTimerParameterHelper_Time::LoopCountFTimerParameterHelper_Frame::LoopCount

  • 支持从下帧开始计数: 详见:FTimerParameterHelper_Time::bSkipCountingThisFrame

  • 我的 FTimerLatentAction_Delay 只需要40字节就可以把所有任务做完了, 但 FTimerDta 有128字节, 大3.2倍!

  • 一种 一劳永逸 的暂停定时器的替代方法:暂停一帧: Rem::Latent::SetTimerPausedOneFrame

  • 还是熟悉的APIs: Rem::Latent::PauseTimerRem::Latent::UnpauseTimerRem::Latent::SetTimerPaused(我们之前见过吗?), Rem::Latent::StopTimerRem::Latent::FindTimerAction

  • 原生支持可重新触发的定时器: Rem::Latent::ResetTimerDelay, 同时支持按时间和帧数来延迟

  • 调用次数补偿可以通过 FTimerParameterHelper_Time::bMaxOncePerFrame 选择性启用, (跟TimerManager一样)

  • 目前还有27个比特是浪费的, 它们是未来的希望!

限制

  • Rem::Latent::FTimerHandle 是32位的, 因为 FLatentActionManager::AddNewAction 只接受 int32, 但它之前在 TimerManageruint64

  • 如果你在 FLatentActionManager::ProcessLatentActions 期间对同一个对象调用了 Rem::Latent::SetTimerForThisTick 可能会发生死循环, 这种情况下,请使用 Rem::Latent::SetTimerForNextTick

  • 如果绑定对象当帧已经更新过, Rem::Latent::SetTimerForThisTick 就不会在对应的 tick group 更新, 这种情况下,考虑使用Rem::Latent::SetTimerForNextTick

  • 为了简单起见,TimeToDelayLoopCountInitialDelay 都只有4字节的大小, 将来可能会考虑用上那些空闲的比特位

  • 需要绑定的对象启用每帧更新,以支持给我们的定时器指定tick group

实例代码

1
2
3
4
5
void UYourObject::DoJob()
{
    auto TimerHandle = Rem::Latent::SetTimerForThisTick(*this,
        FTimerDelegate::CreateUObject(this, &ThisClass::Callback));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void UYourObject::TryDoJobUntilSucceed()
{
    bool bWantToRetry{true};

    ON_SCOPE_EXIT
    {
        if (bWantToRetry)
        {
            Rem::Latent::SetTimerForNextTick(*this, FTimerDelegate::CreateWeakLambda(this,
            [this]
            {
                TryDoJobUntilSucceed();
            }));
        }
    };

    // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void UYourObject::RetriggerableJob()
{
    // ...

    if (!TimerHandle.IsValid())
    {
        TimerHandle = Rem::Latent::SetTimer(*this, FTimerDelegate::CreateWeakLambda(this, [this]
        {
            // ...
            Rem::Latent::StopTimer(*this, TimerHandle);
            TimerHandle = {};
        }), {.TimeToDelay = 1.0f, .LoopCount = 0/*loop infinite*/});
    }
    else
    {
        Rem::Latent::ResetTimerDelay(*this, TimerHandle);
    }
}
This post is licensed under CC BY 4.0 by the author.