前言
在游戏开发中,我们执行一些逻辑经常会出现卡顿,这种卡顿主要是我们游戏在某一帧(Frame)执行太多的任务,比如说我们要读取十个文件,每个文件超过了十万行数据, 这时候,如果我们在一帧内读取完所有文件就很有可能卡顿。 游戏中就有一种技术叫做“分帧加载技术” 可以把一帧执行的任务分摊到N帧进行,从而减少这种一帧内执行太多任务的卡顿。
任务队列
我们把“一个事情的执行”成为任务,任务执行总是先来后到,对应于我们的数据结构 Queue(队列).
总体上分帧加载系统的设计:把要执行的对象(Object)和执行的数据封装为一个 “Task”, 然后在Tick函数通过计数器来判断是否到达下一次执行任务的时候。
任务(FTask)
struct FTask
{
virtual void DoTask() = 0;
FTask() {};
virtual ~FTask() {};
};
虚函数抽象,后面具体执行的各种任务都继承Task
任务管理器(FTaskManager)
struct FTaskManager
{
private:
TQueue<FTask*> TaskQueue;
float ExcuteIntervalTime;
bool bCanExcuteTask;
float CurrentRemainderTime;
static FTaskManager* TaskManager;
public:
FTaskManager(float InExcuteIntervalTime);
FTaskManager();
static FTaskManager* GetInstance();
public:
void AddTask(FTask* Task);
void ClearTaskQueue();
void Tick(float DeltaTime);
void StartExcuteTask();
void SetExcuteIntervalTime(float InExcuteIntervalTime);
};
FTaskManager::FTaskManager(float InExcuteIntervalTime)
{
ExcuteIntervalTime = InExcuteIntervalTime;
CurrentRemainderTime = InExcuteIntervalTime;
}
FTaskManager::FTaskManager():ExcuteIntervalTime(0.15f)
{
}
FTaskManager* FTaskManager::GetInstance()
{
if (nullptr == TaskManager)
{
TaskManager = new FTaskManager();
}
return TaskManager;
}
void FTaskManager::AddTask(FTask* Task)
{
TaskQueue.Enqueue(Task);
}
void FTaskManager::ClearTaskQueue()
{
while (!TaskQueue.IsEmpty())
{
FTask* Task = nullptr;
TaskQueue.Dequeue(Task);
if (nullptr != Task)
{
delete Task;
}
}
}
void FTaskManager::Tick(float DeltaTime)
{
if (bCanExcuteTask)
{
if (TaskQueue.IsEmpty())
{
bCanExcuteTask = false;
return;
}
if (CurrentRemainderTime <= 0.0f)
{
FTask* Task = nullptr;
if (TaskQueue.Dequeue(Task) && nullptr != Task)
{
Task->DoTask();
delete Task;
}
CurrentRemainderTime = ExcuteIntervalTime;
}
else
{
CurrentRemainderTime -= DeltaTime;
}
}
}
void FTaskManager::StartExcuteTask()
{
bCanExcuteTask = true;
}
void FTaskManager::SetExcuteIntervalTime(float InExcuteIntervalTime)
{
ExcuteIntervalTime = InExcuteIntervalTime;
}
(1)我们用TQueue<FTask*> TaskQueue作为任务的队列,从队列尾取出执行的任务,从队列的头添加任务,符合先进先出原则。
(2)我们每个任务执行的间隔为ExcuteIntervalTime; CurrentRemainderTime为计数器,在Tick不断计算剩余执行下一个任务的时间。
执行的任务继承FTask, 封装目标对象(Object)和数据(Data)
这里两个例子:
(1)任务A:一个不断打印Log的任务,需要字符串数据
struct FLogTask : public FTask
{
FString MyPringInfo;
virtual void DoTask() override;
FLogTask(FString PringInfo);
};
FLogTask::FLogTask(FString PringInfo):MyPringInfo(PringInfo)
{
}
void FLogTask::DoTask()
{
UE_LOG(LogTemp, Warning, TEXT("My Print Info = %s"), *MyPringInfo);
}
(2)任务B:促使一个Actor移动一段位移
struct FMoveActorTask : public FTask
{
AActor* MyActor;
FVector MoveVector;
virtual void DoTask() override;
FMoveActorTask(AActor* InMyActor, FVector InMoveVector);
};
FMoveActorTask::FMoveActorTask(AActor* InMyActor, FVector InMoveVector):MyActor(InMyActor),MoveVector(InMoveVector)
{
}
void FMoveActorTask::DoTask()
{
MyActor->SetActorLocation(MyActor->GetActorLocation() + MoveVector);
}
添加执行的任务,tick任务管理器
FTaskManager::GetInstance()->SetExcuteIntervalTime(ExcuteIntervalTime);
FTaskManager::GetInstance()->AddTask(new FLogTask("11111"));
FTaskManager::GetInstance()->AddTask(new FLogTask("22222"));
for(int Index = 0; Index < 10; ++Index)
{
FTaskManager::GetInstance()->AddTask(new FMoveActorTask(this, FVector(0, 0, 100.0f)));
}
FTaskManager::GetInstance()->StartExcuteTask();
// Called every frame
void AMyActor::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
FTaskManager::GetInstance()->Tick(DeltaTime);
}
每个任务执行间隔就是IntervalTime.当然你可以不用时间间隔,可以用帧数间隔 int FrameIntervalCount也行,总体上的思想就是间隔一段时间或者N帧在从任务队列取出一个任务执行,分摊一帧很耗并且数量很多的执行任务(读取大文件,加载大量数据)到N帧中执行