加载流程
调用 ULevelStreamingDynamic::LoadLevelInstance 后,引擎将加载任务放入队列,在后续帧的 Streaming Manager tick 中分阶段处理:
第一阶段,引擎从磁盘异步读取 .umap 包体,完成后将关卡标记为已加载(IsLevelLoaded() == true)。第二阶段,引擎将关卡中的对象添加到 World,实例化 Actor,注册组件,完成后触发 OnLevelLoaded 委托。
整个过程跨越若干帧,具体耗时取决于包体大小和硬件 I/O。调用方代码在这期间照常执行,不会等待。
出参 bOutSuccess 的含义
bOutSuccess 只反映请求是否被引擎接受------路径合法、World 上下文有效。它在 LoadLevelInstance 返回时就已经赋值,与后续的异步加载过程没有任何关系。
bOutSuccess == true 表示加载请求入队成功,不表示关卡已加载,也不表示其中的 Actor 已经存在。
因此,以下写法在逻辑上是错误的:
CarLevelInstance = ULevelStreamingDynamic::LoadLevelInstance(
World, LevelPath, FVector::ZeroVector, FRotator::ZeroRotator, bOutSuccess
);
// bOutSuccess == true,但关卡仍在异步加载中
if (bOutSuccess && CarLevelInstance)
{
// ❌ 此时 World 里没有目标 Actor,必然返回 nullptr
AMyActor* Actor = Cast<AMyActor>(
UGameplayStatics::GetActorOfClass(World, AMyActor::StaticClass())
);
}
用固定延时 Timer 规避也是错误的。加载时长不固定,Timer 到期时关卡不一定加载完毕,这只是把确定性的错误变成了概率性的错误。
正确的操作时机
任何依赖目标关卡内容的操作,都必须等到 OnLevelLoaded 触发后才能执行。这是引擎提供的精确信号,表示关卡中的 Actor 已全部实例化完毕。
方案一:OnLevelLoaded 委托(推荐)
在发起加载时绑定回调,将后续逻辑推迟到委托触发后执行:
// 发起加载,绑定回调
CarLevelInstance = ULevelStreamingDynamic::LoadLevelInstance(..., bOutSuccess);
if (bOutSuccess && CarLevelInstance)
{
CarLevelInstance->SetShouldBeLoaded(true);
CarLevelInstance->SetShouldBeVisible(true);
CarLevelInstance->OnLevelLoaded.AddDynamic(
this, &UMyComponent::OnLevelLoaded
);
}
// 关卡加载完成后,引擎调用此函数
void UMyComponent::OnLevelLoaded()
{
// ✅ Actor 已实例化,可以安全查找和操作
AMyActor* Actor = Cast<AMyActor>(
UGameplayStatics::GetActorOfClass(GetWorld(), AMyActor::StaticClass())
);
}
回调函数必须标记 UFUNCTION(),AddDynamic 通过反射解析函数地址,缺少该宏会导致绑定失败。
方案二:Tick 轮询
若类结构不方便引入回调,可在 Tick 中轮询 IsLevelLoaded()。需要一个布尔标志防止回调逻辑被重复执行,并接受每帧都有轻微检查开销的代价。
void UMyComponent::TickComponent(...)
{
if (CarLevelInstance
&& CarLevelInstance->IsLevelLoaded()
&& !bInitialized)
{
bInitialized = true;
OnLevelLoaded();
}
}
注意 IsLevelLoaded() 只对应加载流程的第一阶段------包体读取完毕。Actor 的实例化发生在此之后,在 IsLevelLoaded() 返回 true 的同帧操作 Actor 仍然可能失败。OnLevelLoaded 委托的触发时机更晚,也更准确,这是优先推荐委托方案的原因。