Level translation when streaming not working

This question explains & answer how to stream in a level at a location. As described, I set the LevelTransform on the StreamingLevel before requesting it to be loaded. However, despite the transform being correct, the level is still based around the origin.

Digging in to it I see that when we come to UWorld::AddToWorld, the bAlreadyMovedActors flag has been set to true so the transform is never applied. Further looking reveals it gets set to true in this bit of code:

UWorld* PIELevelWorld = UWorld::DuplicateWorldForPIE(NonPrefixedLevelName, PersistentWorld);
if (PIELevelWorld)
{
	PIELevelWorld->PersistentLevel->bAlreadyMovedActors = true; // As we have duplicated the world, the actors will already have been transformed
	check(PendingUnloadLevel == NULL);
	SetLoadedLevel(PIELevelWorld->PersistentLevel);

	// Broadcast level loaded event to blueprints
	OnLevelLoaded.Broadcast();

	return true;
}

This is inside ULevelStreaming::RequestLevel. I’m not really sure what the comment means, but the level is definitely the new level being streamed in (why it is called the PersistentLevel I’m not sure, it isn’t the persistent level - just a duplication of the persistent world…).

Also, the fact that it is talking about PIE (PlayInEditor) makes me worry this is going to be an editor only issue…

How do I correctly apply the transform to the level?

Update

Commenting out the setting of bAlreadyMovedActors in the above sample does “resolve” the issue, so it is at least related to that.

Yeah, it’s a known issue, basically setting level transform in the blueprints does not work in PIE. Editor assumes that loaded sub-levels are already been transformed, so he does not need to apply transformation again when duplicating sub-levels for PIE session. We will look into fixing it, as a workaround you can PIE as standalone game, in this case blueprint transformations will work, because game will actually load sub-levels instead of duplicating them from editor world.

Ah ok, what down sides are there to commenting out the bAlreadyMovedActors in the above sample? Didn’t see any issues but obviously didn’t test exhaustively. Would rather not use the standalone if possible…

It will not work correctly when you have transform applied in the Editor. In Levels details tab you can set sub-level transform. But if you don’t use editor transforms then ignoring bAlreadyMovedActors will work fine.

Holp this can help you: https://youtu.be/hLIs0Bou7mU

This is still an issue in 4.21. I think i would like to also ignore bAlreadyMovedActors but would hate to have to fork the engine just for this. Any plans to address this issue?

Why can’t we have this exposed as a boolean option in editor for each sub-level and if we tick it off (to enable dynamic transforms) then the editor transform box becomes disabled to show that it is being ignored…

can it be that this problem is still not resolved in 5.2? At least I still have it. Any known fixes yet?

After a morning of struggling with this, I think I have a solution.

I make the solution in C++, this is the code:

void ACorridorActor::LoadLevel(const FName LevelName, const FTransform& LevelTransform)
{
	auto CurrentLevel = UGameplayStatics::GetStreamingLevel(GetWorld(), LevelName);

	if (CurrentLevel && !CurrentLevel->IsLevelLoaded())
	{
		LoadedLevelTransform = LevelTransform;
		LoadedLevelName = LevelName;
		
		FLatentActionInfo LatentInfo;
		LatentInfo.Linkage = 0;
		LatentInfo.UUID = 1;
		LatentInfo.CallbackTarget = this;
		LatentInfo.ExecutionFunction = "CallbackOnLevelLoaded";
		UGameplayStatics::LoadStreamLevel(this, LevelName, false, false, LatentInfo);
	}
	else if (CurrentLevel == nullptr)
	{
		UE_LOG(LogTemp, Error, TEXT("ERROR: Could not load level %s, level not found!"), *LevelName.ToString());
	}
}

void ACorridorActor::CallbackOnLevelLoaded()
{
	auto CurrentLevel = UGameplayStatics::GetStreamingLevel(GetWorld(), LoadedLevelName);

	if (CurrentLevel)
	{
#if WITH_EDITOR
		FLevelUtils::SetEditorTransform(CurrentLevel, LoadedLevelTransform);
#endif
		CurrentLevel->LevelTransform = LoadedLevelTransform;
		CurrentLevel->SetShouldBeVisible(true);

		OnLevelLoaded.Broadcast();
	}
}

void ACorridorActor::UnloadLevel(FName LevelName)
{
	const auto CurrentLevel = UGameplayStatics::GetStreamingLevel(GetWorld(), LevelName);

	if (CurrentLevel && CurrentLevel->IsLevelLoaded())
	{
		FLatentActionInfo LatentInfo;
		LatentInfo.Linkage = 0;
		LatentInfo.UUID = 0;
		LatentInfo.CallbackTarget = this;
		LatentInfo.ExecutionFunction = "CallbackOnLevelUnloaded";
		UGameplayStatics::UnloadStreamLevel(this, LevelName, LatentInfo, false);
	}
}

void ACorridorActor::CallbackOnLevelUnloaded()
{
#if WITH_EDITOR
	auto CurrentLevel = UGameplayStatics::GetStreamingLevel(GetWorld(), LoadedLevelName);

	if (CurrentLevel && !CurrentLevel->IsLevelLoaded())
	{
		// To avoid warnings in the editor, should be moved to Identity at this point. 
		FLevelUtils::SetEditorTransform(CurrentLevel, FTransform());
	}
#endif
	
	OnLevelUnloaded.Broadcast();
}

Tested inside and outside the editor.

Hope this helps someone.