Unreal源码学习(一)Tick逻辑总结
本篇文章主要明确以下几大类问题:C++ 中的 Tick 和蓝图中的 Tick 的关系?Tick 的分组有何作用?不同 Tick 函数之间的如何处理依赖关系?Tick,TickableObject,Timer 在同一帧中的以何种顺序执行?
我们随便创建一个蓝图的时候都会看到有个 Tick 节点。而使用UE4在C++中创建一个Actor的时候也会自动帮你把 Tick 方法重写好。
蓝图中的Tick
void ATestActor::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}简单翻看AActor的源码就能得知其实蓝图中的 Tick 是实现了C++中的 ReceiveTick:
UFUNCTION(BlueprintImplementableEvent, meta=(DisplayName = "Tick"))
void ReceiveTick(float DeltaSeconds);只不过为了统一逻辑用DisplayName在蓝图中把它显示成了Tick。而此方法在AActor的Tick方法中被调用:
void AActor::Tick( float DeltaSeconds )
{
if (GetClass()->HasAnyClassFlags(CLASS_CompiledFromBlueprint) || !GetClass()->HasAnyClassFlags(CLASS_Native))
{
// Blueprint code outside of the construction script should not run in the editor
// Allow tick if we are not a dedicated server, or we allow this tick on dedicated servers
if (GetWorldSettings() != nullptr && (bAllowReceiveTickEventOnDedicatedServer || !IsRunningDedicatedServer()))
{
ReceiveTick(DeltaSeconds);
}
//....
}
}在这里我们还能看到是有判断的,如果你是一个纯C++的AActor是不会走ReceiveTick的。
虚幻中Tick是可以指定分组的。从源码中我们能看出这个分组主要针对的问题就是我们自己的Tick逻辑和物理模拟如何相处的问题:
enum ETickingGroup
{
/** Any item that needs to be executed before physics simulation starts. */
TG_PrePhysics UMETA(DisplayName="Pre Physics"),
/** Special tick group that starts physics simulation. */
TG_StartPhysics UMETA(Hidden, DisplayName="Start Physics"),
/** Any item that can be run in parallel with our physics simulation work. */
TG_DuringPhysics UMETA(DisplayName="During Physics"),
/** Special tick group that ends physics simulation. */
TG_EndPhysics UMETA(Hidden, DisplayName="End Physics"),
/** Any item that needs rigid body and cloth simulation to be complete before being executed. */
TG_PostPhysics UMETA(DisplayName="Post Physics"),
/** Any item that needs the update work to be done before being ticked. */
TG_PostUpdateWork UMETA(DisplayName="Post Update Work"),
/** Catchall for anything demoted to the end. */
TG_LastDemotable UMETA(Hidden, DisplayName = "Last Demotable"),
/** Special tick group that is not actually a tick group. After every tick group this is repeatedly re-run until there are no more newly spawned items to run. */
TG_NewlySpawned UMETA(Hidden, DisplayName="Newly Spawned"),
TG_MAX,
};可以看到一般写Gameplay我们能够选择的分组主要就是四个:
TG_PrePhysics 在物理模拟前运行TG_DuringPhysics 和物理模拟一起运行TG_PostPhysics 在物理模拟之后运行TG_PostUpdateWork 在所有Tick 运行之后(包括Timer和Tickableobject)
光看注释当然不能说明什么,具体执行时的代码在UWorld::Tick中(此处只截取有关部分):
void UWorld::Tick( ELevelTick TickType, float DeltaSeconds )
{
//...
for (int32 i = 0; i < LevelCollections.Num(); ++i)
{
//...
// If caller wants time update only, or we are paused, skip the rest.
if (bDoingActorTicks)
{
// Actually tick actors now that context is set up
SetupPhysicsTickFunctions(DeltaSeconds);
TickGroup = TG_PrePhysics; // reset this to the start tick group
FTickTaskManagerInterface::Get().StartFrame(this, DeltaSeconds, TickType, LevelsToTick);
SCOPE_CYCLE_COUNTER(STAT_TickTime);
{
SCOPE_TIME_GUARD_MS(TEXT(&#34;UWorld::Tick - TG_PrePhysics&#34;), 10);
SCOPE_CYCLE_COUNTER(STAT_TG_PrePhysics);
CSV_SCOPED_TIMING_STAT_EXCLUSIVE(PrePhysicsMisc);
CSV_SCOPED_SET_WAIT_STAT(PrePhysics);
RunTickGroup(TG_PrePhysics);
}
bInTick = false;
EnsureCollisionTreeIsBuilt();
bInTick = true;
{
SCOPE_CYCLE_COUNTER(STAT_TG_StartPhysics);
SCOPE_TIME_GUARD_MS(TEXT(&#34;UWorld::Tick - TG_StartPhysics&#34;), 10);
CSV_SCOPED_TIMING_STAT_EXCLUSIVE(StartPhysicsMisc);
CSV_SCOPED_SET_WAIT_STAT(StartPhysics);
RunTickGroup(TG_StartPhysics);
}
{
SCOPE_CYCLE_COUNTER(STAT_TG_DuringPhysics);
SCOPE_TIME_GUARD_MS(TEXT(&#34;UWorld::Tick - TG_DuringPhysics&#34;), 10);
CSV_SCOPED_TIMING_STAT_EXCLUSIVE(DuringPhysicsMisc);
CSV_SCOPED_SET_WAIT_STAT(DuringPhysics);
RunTickGroup(TG_DuringPhysics, false); // No wait here, we should run until idle though. We don&#39;t care if all of the async ticks are done before we start running post-phys stuff
}
TickGroup = TG_EndPhysics; // set this here so the current tick group is correct during collision notifies, though I am not sure it matters. &#39;cause of the false up there^^^
{
SCOPE_CYCLE_COUNTER(STAT_TG_EndPhysics);
SCOPE_TIME_GUARD_MS(TEXT(&#34;UWorld::Tick - TG_EndPhysics&#34;), 10);
CSV_SCOPED_TIMING_STAT_EXCLUSIVE(EndPhysicsMisc);
CSV_SCOPED_SET_WAIT_STAT(EndPhysics);
RunTickGroup(TG_EndPhysics);
}
{
SCOPE_CYCLE_COUNTER(STAT_TG_PostPhysics);
SCOPE_TIME_GUARD_MS(TEXT(&#34;UWorld::Tick - TG_PostPhysics&#34;), 10);
CSV_SCOPED_TIMING_STAT_EXCLSIVE(PostPhysicsMisc);
CSV_SCOPED_SET_WAIT_STAT(PostPhysics);
RunTickGroup(TG_PostPhysics);
}
}
else if( bIsPaused )
{
FTickTaskManagerInterface::Get().RunPauseFrame(this, DeltaSeconds, LEVELTICK_PauseTick, LevelsToTick);
}
// We only want to run the following once, so only run it for the source level collection.
if (LevelCollections.GetType() == ELevelCollectionType::DynamicSourceLevels)
{
//...
{
SCOPE_CYCLE_COUNTER(STAT_TickableTickTime);
if (TickType != LEVELTICK_TimeOnly && !bIsPaused)
{
SCOPE_TIME_GUARD_MS(TEXT(&#34;UWorld::Tick - TimerManager&#34;), 5);
STAT(FScopeCycleCounter Context(GetTimerManager().GetStatId());)
GetTimerManager().Tick(DeltaSeconds);
}
{
SCOPE_TIME_GUARD_MS(TEXT(&#34;UWorld::Tick - TickObjects&#34;), 5);
FTickableGameObject::TickObjects(this, TickType, bIsPaused, DeltaSeconds);
}
}
}
if (bDoingActorTicks)
{
SCOPE_CYCLE_COUNTER(STAT_TickTime);
{
SCOPE_CYCLE_COUNTER(STAT_TG_PostUpdateWork);
SCOPE_TIME_GUARD_MS(TEXT(&#34;UWorld::Tick - PostUpdateWork&#34;), 5);
CSV_SCOPED_SET_WAIT_STAT(PostUpdateWork);
RunTickGroup(TG_PostUpdateWork);
}
{
SCOPE_CYCLE_COUNTER(STAT_TG_LastDemotable);
SCOPE_TIME_GUARD_MS(TEXT(&#34;UWorld::Tick - TG_LastDemotable&#34;), 5);
CSV_SCOPED_SET_WAIT_STAT(LastDemotable);
RunTickGroup(TG_LastDemotable);
}
FTickTaskManagerInterface::Get().EndFrame();
}
}
}通过 RunTickGroup 来按顺序跑每组的 tick。需要注意的是在跑 TG_DuringPhysics 与其他的不一样通过传入第二个参数来表示不需要等待 TG_DuringPhysics 组中的 Tick 完成直接开始下一组的 tick。
在一系列的 RunTickGroup 执行顺序里面穿插了两个我们也比较熟悉的需要 tick 的功能:
GetTimerManager().Tick(DeltaSeconds);
FTickableGameObject::TickObjects(this, TickType, bIsPaused, DeltaSeconds);一个是 Timer 的 tick。这个就是字面意思很好理解。
TickableGameObject是为了那些实现 FTickableGameObject 的对象准备的。
通过我们最常用的 AController 和 APawn 就能对 Tick 的这些逻辑有很好的理解。
与其他单独的Actor不同,AController 和 APawn 有明显的关联关系。
void AController::AddPawnTickDependency(APawn* NewPawn)
{
if (NewPawn != NULL)
{
bool bNeedsPawnPrereq = true;
UPawnMovementComponent* PawnMovement = NewPawn->GetMovementComponent();
if (PawnMovement && PawnMovement->PrimaryComponentTick.bCanEverTick)
{
PawnMovement->PrimaryComponentTick.AddPrerequisite(this, this->PrimaryActorTick);
// Don&#39;t need a prereq on the pawn if the movement component already sets up a prereq.
if (PawnMovement->bTickBeforeOwner || NewPawn->PrimaryActorTick.GetPrerequisites().Contains(FTickPrerequisite(PawnMovement, PawnMovement->PrimaryComponentTick)))
{
bNeedsPawnPrereq = false;
}
}
if (bNeedsPawnPrereq)
{
NewPawn->PrimaryActorTick.AddPrerequisite(this, this->PrimaryActorTick);
}
}
}在 AController 的 AddPawnTickDependency 方法中我们可以明确的看到如何来处理这种关系。
PawnMovement 将 AController 作为前置依赖,而 PawnMovement 又是 NewPawn 的前置依赖。这样就组成了一条依赖链。我们可以通过这种方式来组织具有关联的 Tick 逻辑。
如果我们想为一个 UObject 的子类添加 Tick 我们还有另一种选择,就是利用 FTickableGameObject。这里我们可以拿虚幻中的 AI 子系统作为参考:
class AIMODULE_API UAISubsystem : public UObject, public FTickableGameObject
{
GENERATED_BODY()
//...
public:
//...
// FTickableGameObject begin
virtual UWorld* GetTickableGameObjectWorld() const override { return GetWorldFast(); }
virtual void Tick(float DeltaTime) override {}
virtual ETickableTickType GetTickableTickType() const override;
virtual TStatId GetStatId() const override;
// FTickableGameObject end
};UAISubsystem 继承 FTickableGameObject 并实现几个需要的方法就可以了。
不过此种方式实现的 Tick 不能配置上面所讲的 TickGroup 等参数。只是能把你的 Tick 逻辑注册到引擎中。然后在 TG_PostPhysics 后,TG_PostUpdateWork 前执行。
总结
蓝图中的 Tick 实质上是 C++ Tick 中所调用的一部分。Tick 分组可以用来控制 Tick 在当前帧的什么阶段执行。具有依赖关系的 Tick 之间可以通过 AddPrerequisite 来设置先后顺序。除了像 Actor 那样使用 FTickFunction (PrimaryActorTick)还可以直接继承 FTickableGameObject 来向 UObject 添加 Tick 能力。
页:
[1]