bmh/FlightSimulation/Plugins/CesiumForUnreal_5.4/Source/CesiumRuntime/Private/CesiumSubLevelSwitcherComponent.cpp
2025-02-07 22:52:32 +08:00

366 lines
12 KiB
C++

// Copyright 2020-2024 CesiumGS, Inc. and Contributors
#include "CesiumSubLevelSwitcherComponent.h"
#include "CesiumCommon.h"
#include "CesiumRuntime.h"
#include "CesiumSubLevelComponent.h"
#include "Engine/LevelStreaming.h"
#include "Engine/World.h"
#include "LevelInstance/LevelInstanceActor.h"
#include "LevelInstance/LevelInstanceLevelStreaming.h"
#include "LevelInstance/LevelInstanceSubsystem.h"
#include "Runtime/Launch/Resources/Version.h"
#if WITH_EDITOR
#include "Editor.h"
#endif
namespace {
FString GetActorLabel(AActor* pActor) {
if (!IsValid(pActor))
return TEXT("<none>");
#if WITH_EDITOR
return pActor->GetActorLabel();
#else
return pActor->GetName();
#endif
}
} // namespace
UCesiumSubLevelSwitcherComponent::UCesiumSubLevelSwitcherComponent() {
this->PrimaryComponentTick.bCanEverTick = true;
}
void UCesiumSubLevelSwitcherComponent::RegisterSubLevel(
ALevelInstance* pSubLevel) noexcept {
this->_sublevels.AddUnique(pSubLevel);
// Do extra checks on the next tick so that if we're in a game and this level
// is already loaded and shouldn't be, we can unload it.
this->_doExtraChecksOnNextTick = true;
// In the Editor, sub-levels other than the target must initially be hidden.
#if WITH_EDITOR
if (GEditor && IsValid(this->GetWorld()) &&
!this->GetWorld()->IsGameWorld()) {
if (this->_pTarget != pSubLevel) {
pSubLevel->SetIsTemporarilyHiddenInEditor(true);
}
}
#endif
}
void UCesiumSubLevelSwitcherComponent::UnregisterSubLevel(
ALevelInstance* pSubLevel) noexcept {
this->_sublevels.Remove(pSubLevel);
// Next tick, we need to check if the target is still registered, in case this
// method call just removed it. But we can't actually do the check here
// because the Editor UI goes through an unregister/re-register cycle
// _constantly_, and we don't want to forget the target sub-level just because
// it was edited in the UI.
this->_doExtraChecksOnNextTick = true;
}
TArray<ALevelInstance*>
UCesiumSubLevelSwitcherComponent::GetRegisteredSubLevels() const noexcept {
TArray<ALevelInstance*> result;
result.Reserve(this->_sublevels.Num());
for (const TWeakObjectPtr<ALevelInstance>& pWeak : this->_sublevels) {
ALevelInstance* p = pWeak.Get();
if (p)
result.Add(p);
}
return result;
}
ALevelInstance*
UCesiumSubLevelSwitcherComponent::GetCurrentSubLevel() const noexcept {
return this->_pCurrent.Get();
}
ALevelInstance*
UCesiumSubLevelSwitcherComponent::GetTargetSubLevel() const noexcept {
return this->_pTarget.Get();
}
void UCesiumSubLevelSwitcherComponent::SetTargetSubLevel(
ALevelInstance* pLevelInstance) noexcept {
if (this->_pTarget != pLevelInstance) {
if (pLevelInstance) {
UE_LOG(
LogCesium,
Display,
TEXT("New target sub-level %s."),
*GetActorLabel(pLevelInstance));
} else {
UE_LOG(LogCesium, Display, TEXT("New target sub-level <none>"));
}
this->_pTarget = pLevelInstance;
this->_isTransitioningSubLevels = true;
}
}
void UCesiumSubLevelSwitcherComponent::TickComponent(
float DeltaTime,
enum ELevelTick TickType,
FActorComponentTickFunction* ThisTickFunction) {
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
if (this->_doExtraChecksOnNextTick) {
if (this->_pTarget != nullptr &&
this->_sublevels.Find(this->_pTarget) == INDEX_NONE) {
// Target level is no longer registered, so the new target is "none".
this->SetTargetSubLevel(nullptr);
}
// In game, make sure that any sub-levels that aren't pCurrent or pTarget
// are unloaded. This is primarily needed because ALevelInstances are loaded
// by default and there doesn't seem to be any way to disable this. In the
// Editor, levels pretty much stay loaded all the time.
if (this->GetWorld()->IsGameWorld()) {
bool anyLevelsStillLoaded = false;
for (int32 i = 0; i < this->_sublevels.Num(); ++i) {
ALevelInstance* pSubLevel = this->_sublevels[i].Get();
if (!IsValid(pSubLevel))
continue;
if (pSubLevel == this->_pCurrent || pSubLevel == this->_pTarget)
continue;
ULevelStreaming* pStreaming =
this->_getLevelStreamingForSubLevel(pSubLevel);
ELevelStreamingState state = IsValid(pStreaming)
? pStreaming->GetLevelStreamingState()
: ELevelStreamingState::Unloaded;
switch (state) {
case ELevelStreamingState::Loading:
case ELevelStreamingState::MakingInvisible:
case ELevelStreamingState::MakingVisible:
anyLevelsStillLoaded = true;
break;
case ELevelStreamingState::LoadedNotVisible:
case ELevelStreamingState::LoadedVisible:
pSubLevel->UnloadLevelInstance();
anyLevelsStillLoaded = true;
break;
case ELevelStreamingState::FailedToLoad:
case ELevelStreamingState::Removed:
case ELevelStreamingState::Unloaded:
break;
}
}
if (anyLevelsStillLoaded) {
// Don't do anything else until the levels are unloaded.
return;
}
}
this->_doExtraChecksOnNextTick = false;
}
#if WITH_EDITOR
UWorld* pWorld = this->GetWorld();
if (!IsValid(pWorld))
return;
if (!this->GetWorld()->IsGameWorld()) {
this->_updateSubLevelStateEditor();
return;
}
#endif
this->_updateSubLevelStateGame();
}
void UCesiumSubLevelSwitcherComponent::_updateSubLevelStateGame() {
if (this->_isTransitioningSubLevels && this->_pCurrent == this->_pTarget) {
// It's possible that the pCurrent sub-level was active, then we briefly set
// pTarget to something else to trigger an unload of pCurrent, and then
// immediately set pTarget back to pCurrent. So we detect that here.
this->_pCurrent = nullptr;
}
if (this->_pCurrent == this->_pTarget) {
// We already match the desired state, so there's nothing to do!
return;
}
this->_isTransitioningSubLevels = false;
if (this->_pCurrent != nullptr) {
// Work toward unloading the current level.
ULevelStreaming* pStreaming =
this->_getLevelStreamingForSubLevel(this->_pCurrent.Get());
ELevelStreamingState state = ELevelStreamingState::Unloaded;
if (IsValid(pStreaming)) {
state = pStreaming->GetLevelStreamingState();
} else if (this->_pCurrent->GetWorldAsset().IsNull()) {
// There is no level associated with the target at all, so mark it
// unloaded but also deactivate it for the benefit of the Editor UI.
this->_pCurrent->UnloadLevelInstance();
}
switch (state) {
case ELevelStreamingState::Loading:
case ELevelStreamingState::MakingInvisible:
case ELevelStreamingState::MakingVisible:
// Wait for these transitions to finish before doing anything further.
// TODO: maybe we can cancel these transitions somehow?
UE_LOG(
LogCesium,
Log,
TEXT(
"Waiting for sub-level %s to transition out of an intermediate state while unloading it."),
*GetActorLabel(this->_pCurrent.Get()));
this->_isTransitioningSubLevels = true;
break;
case ELevelStreamingState::LoadedNotVisible:
case ELevelStreamingState::LoadedVisible:
UE_LOG(
LogCesium,
Display,
TEXT("Starting unload of sub-level %s."),
*GetActorLabel(this->_pCurrent.Get()));
this->_isTransitioningSubLevels = true;
this->_pCurrent->UnloadLevelInstance();
break;
case ELevelStreamingState::FailedToLoad:
case ELevelStreamingState::Removed:
case ELevelStreamingState::Unloaded:
UE_LOG(
LogCesium,
Display,
TEXT("Finished unloading sub-level %s."),
*GetActorLabel(this->_pCurrent.Get()));
this->_pCurrent = nullptr;
break;
}
}
if (this->_pCurrent == nullptr && this->_pTarget != nullptr) {
// Now that the current level is unloaded, work toward loading the target
// level.
// At this point there's no Current sub-level, so it's safe to activate the
// Target one even though it's not loaded yet. This way, by the time the
// level _is_ loaded, it will be at the right location because the
// georeference has been updated.
UCesiumSubLevelComponent* pTargetComponent =
this->_pTarget->FindComponentByClass<UCesiumSubLevelComponent>();
if (pTargetComponent)
pTargetComponent->UpdateGeoreferenceIfSubLevelIsActive();
ULevelStreaming* pStreaming =
this->_getLevelStreamingForSubLevel(this->_pTarget.Get());
ELevelStreamingState state = ELevelStreamingState::Unloaded;
if (IsValid(pStreaming)) {
state = pStreaming->GetLevelStreamingState();
} else if (this->_pTarget.Get()->GetWorldAsset().IsNull()) {
// There is no level associated with the target at all, so mark it failed
// to load because this is as loaded as it will ever be.
state = ELevelStreamingState::FailedToLoad;
}
switch (state) {
case ELevelStreamingState::Loading:
case ELevelStreamingState::MakingInvisible:
case ELevelStreamingState::MakingVisible:
// Wait for these transitions to finish before doing anything further.
UE_LOG(
LogCesium,
Log,
TEXT(
"Waiting for sub-level %s to transition out of an intermediate state while loading it."),
*GetActorLabel(this->_pTarget.Get()));
this->_isTransitioningSubLevels = true;
break;
case ELevelStreamingState::FailedToLoad:
case ELevelStreamingState::LoadedNotVisible:
case ELevelStreamingState::LoadedVisible:
// Loading complete!
UE_LOG(
LogCesium,
Display,
TEXT("Finished loading sub-level %s."),
*GetActorLabel(this->_pTarget.Get()));
// Double-check that we're not actively trying to unload this level
// already. If we are, wait longer.
if ((IsValid(pStreaming) && pStreaming->ShouldBeLoaded()) ||
this->_pTarget.Get()->GetWorldAsset().IsNull()) {
this->_pCurrent = this->_pTarget;
} else {
this->_isTransitioningSubLevels = true;
}
break;
case ELevelStreamingState::Removed:
case ELevelStreamingState::Unloaded:
// Start loading this level
UE_LOG(
LogCesium,
Display,
TEXT("Starting load of sub-level %s."),
*GetActorLabel(this->_pTarget.Get()));
this->_isTransitioningSubLevels = true;
this->_pTarget.Get()->LoadLevelInstance();
break;
}
}
}
#if WITH_EDITOR
void UCesiumSubLevelSwitcherComponent::_updateSubLevelStateEditor() {
if (this->_pTarget == this->_pCurrent) {
// We already match the desired state, so there's nothing to do!
return;
}
if (this->_pCurrent != nullptr) {
this->_pCurrent.Get()->SetIsTemporarilyHiddenInEditor(true);
this->_pCurrent = nullptr;
}
if (this->_pTarget != nullptr) {
UCesiumSubLevelComponent* pTargetComponent =
this->_pTarget.Get()->FindComponentByClass<UCesiumSubLevelComponent>();
if (pTargetComponent)
pTargetComponent->UpdateGeoreferenceIfSubLevelIsActive();
this->_pTarget.Get()->SetIsTemporarilyHiddenInEditor(false);
this->_pCurrent = this->_pTarget;
}
}
#endif
ULevelStreaming*
UCesiumSubLevelSwitcherComponent::_getLevelStreamingForSubLevel(
ALevelInstance* SubLevel) const {
if (!IsValid(SubLevel))
return nullptr;
ULevelStreaming* const* ppStreaming =
GetWorld()->GetStreamingLevels().FindByPredicate(
[SubLevel](ULevelStreaming* pStreaming) {
ULevelStreamingLevelInstance* pInstanceStreaming =
Cast<ULevelStreamingLevelInstance>(pStreaming);
if (!pInstanceStreaming)
return false;
return pInstanceStreaming->GetLevelInstance() == SubLevel;
});
return ppStreaming ? *ppStreaming : nullptr;
}