游戏的任务队列之分帧加载(在UE4的实现)

前言

在游戏开发中,我们执行一些逻辑经常会出现卡顿,这种卡顿主要是我们游戏在某一帧(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帧中执行

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值