diff --git a/Source/SPUD/Private/SpudRoamingActorSubsystem.cpp b/Source/SPUD/Private/SpudRoamingActorSubsystem.cpp new file mode 100644 index 0000000..812a577 --- /dev/null +++ b/Source/SPUD/Private/SpudRoamingActorSubsystem.cpp @@ -0,0 +1,398 @@ +#include "SpudRoamingActorSubsystem.h" + +#include "SpudSubsystem.h" +#include "WorldPartition/WorldPartitionSubsystem.h" + +// Console variable to toggle debug drawing of WP cell cache bounds at runtime +// Usage: RoamingActorSubsystem.DebugDrawCells 1 +static TAutoConsoleVariable CVarDebugDrawCells( + TEXT("RoamingActorSubsystem.DebugDrawCells"), + false, + TEXT("Draw debug boxes for WP cell cache") +); + +void USpudRoamingActorSubsystem::Initialize(FSubsystemCollectionBase& Collection) +{ + Super::Initialize(Collection); + + const UGameInstance* GI = GetWorld()->GetGameInstance(); + if (!GI) return; + + CachedSpudSubsystem = GI->GetSubsystem(); + + if (USpudSubsystem* Spud = CachedSpudSubsystem.Get()) + { + // OnLevelStore fires when SPUD saves a specific streaming level + Spud->OnLevelStore.AddDynamic(this, &ThisClass::OnLevelStore); + // Track streaming level load/unload to keep cell state cache up to date + Spud->PostLoadStreamingLevel.AddDynamic(this, &ThisClass::OnPostLoadStreamingLevel); + Spud->PreUnloadStreamingLevel.AddDynamic(this, &ThisClass::OnPreUnloadStreamingLevel); + Spud->PostUnloadStreamingLevel.AddDynamic(this, &ThisClass::OnPostUnloadStreamingLevel); + + } +} + +void USpudRoamingActorSubsystem::Deinitialize() +{ + if (USpudSubsystem* Spud = CachedSpudSubsystem.Get()) + { + Spud->OnLevelStore.RemoveDynamic(this, &ThisClass::OnLevelStore); + Spud->PostLoadStreamingLevel.RemoveDynamic(this, &ThisClass::OnPostLoadStreamingLevel); + Spud->PreUnloadStreamingLevel.RemoveDynamic(this, &ThisClass::OnPreUnloadStreamingLevel); + Spud->PostUnloadStreamingLevel.RemoveDynamic(this, &ThisClass::OnPostUnloadStreamingLevel); + } + + CachedSpudSubsystem = nullptr; + TrackedActors.Empty(); + CellCache.Empty(); + + Super::Deinitialize(); +} + +// Only create this subsystem in game worlds +bool USpudRoamingActorSubsystem::ShouldCreateSubsystem(UObject* Outer) const +{ + const UWorld* World = Cast(Outer); + if (!World || !World->IsGameWorld()) return false; + + // Only create on server / standalone + return World->GetNetMode() != NM_Client; +} + +void USpudRoamingActorSubsystem::RegisterActor(AActor* Actor) +{ + if (!Actor) return; + + // Prevent duplicate registration + if (TrackedActors.ContainsByPredicate([Actor](const FTrackedActor& T) { return T.Actor == Actor; })) + return; + + FTrackedActor& NewTracked = TrackedActors.AddDefaulted_GetRef(); + NewTracked.Actor = Actor; + // LastValidCellName will be populated on the first Tick +} + +void USpudRoamingActorSubsystem::UnregisterActor(AActor* Actor) +{ + if (!Actor) return; + + TrackedActors.RemoveAll([Actor](const FTrackedActor& T) { return T.Actor == Actor; }); +} + +// Called by SPUD when it is about to save a specific streaming level. +// We store each tracked actor into whichever cell it physically occupies, +// falling back to LastValidCellName if no cell is found at the current location. +void USpudRoamingActorSubsystem::OnLevelStore(const FString& LevelName) +{ + // Only save on the authority + if (!GetWorld()->GetAuthGameMode()) return; + + USpudSubsystem* Spud = CachedSpudSubsystem.Get(); + if (!Spud) return; + + TArray ActorsToDestroy; + + for (FTrackedActor& Tracked : TrackedActors) + { + if (!Tracked.Actor.IsValid()) continue; + + FString CurrentCell; + bool bIsActivated = false; + FindCellForLocation(Tracked.Actor->GetActorLocation(), CurrentCell, bIsActivated); + + const FString& TargetCell = !CurrentCell.IsEmpty() + ? CurrentCell + : Tracked.LastValidCellName; + + if (TargetCell.IsEmpty()) continue; + if (TargetCell != LevelName) continue; + + const FCachedCellData* CellData = CellCache.FindByPredicate([&TargetCell](const FCachedCellData& D) + { + return D.LevelName == TargetCell; + }); + + if (CellData && CellData->bPendingUnload) + { + // Cell is about to unload, clamp, save and destroy + SaveAndDestroyActor(Tracked, TargetCell, Spud, ActorsToDestroy); + } + else + { + // Normal save, just store without destroying + Spud->StoreActorByCell(Tracked.Actor.Get(), TargetCell); + } + } + + for (AActor* Actor : ActorsToDestroy) + { + //Actor->SetNetDormancy(DORM_DormantAll); + Actor->Destroy(); + } +} + +void USpudRoamingActorSubsystem::OnPreUnloadStreamingLevel(const FName& LevelName) +{ + for (auto& Data : CellCache) + { + if (Data.LevelName == LevelName.ToString()) + { + Data.bPendingUnload = true; + break; + } + } +} + +void USpudRoamingActorSubsystem::OnPostUnloadStreamingLevel(const FName& LevelName) +{ + for (auto& Data : CellCache) + { + if (Data.LevelName == LevelName.ToString()) + { + Data.bPendingUnload = false; + break; + } + } + + OnStreamingStateUpdated(); +} + +void USpudRoamingActorSubsystem::OnPostLoadStreamingLevel(const FName& LevelName) +{ + for (auto& Data : CellCache) + { + if (Data.LevelName == LevelName.ToString()) + { + Data.bPendingUnload = false; + break; + } + } + + OnStreamingStateUpdated(); +} + +// Keeps the cell state cache in sync with the current WP streaming state. +// If the number of valid cells has changed, performs a full rebuild. +// Otherwise just refreshes the State field of each cached entry. +void USpudRoamingActorSubsystem::OnStreamingStateUpdated() +{ + UWorldPartitionSubsystem* WorldPartitionSubsystem = GetWorld()->GetSubsystem(); + if (!WorldPartitionSubsystem) return; + + // Count only cells with valid content + int32 ActualCellCount = 0; + WorldPartitionSubsystem->ForEachWorldPartition([&ActualCellCount](const UWorldPartition* WorldPartition) -> bool + { + if (WorldPartition) + WorldPartition->RuntimeHash->ForEachStreamingCells([&ActualCellCount](const UWorldPartitionRuntimeCell* Cell) -> bool + { + //if (Cell && Cell->GetContentBounds().IsValid) + ++ActualCellCount; + return true; + }); + return true; + }); + + // Cell count mismatch means cells were added or removed + if (ActualCellCount != CellCache.Num()) + { + RebuildCellCache(); + return; + } + + // Update only the streaming state of each cached cell + for (auto& Data : CellCache) + Data.State = Data.Cell->GetCurrentState(); +} + +// Rebuilds the cell cache from scratch by iterating all WP streaming cells. +// Cells without valid content bounds are skipped +void USpudRoamingActorSubsystem::RebuildCellCache() +{ + CellCache.Empty(); + + UWorldPartitionSubsystem* WorldPartitionSubsystem = GetWorld()->GetSubsystem(); + if (!WorldPartitionSubsystem) return; + + auto ForEachCellFunction = [this](const UWorldPartitionRuntimeCell* Cell) -> bool + { + if (Cell) + { + // Skip cells with no content + //if (!Cell->GetContentBounds().IsValid) return true; + + FCachedCellData& Data = CellCache.AddDefaulted_GetRef(); + Data.Cell = Cell; + Data.Bounds = Cell->GetStreamingBounds(); + Data.LevelName = USpudState::GetLevelName(Cell); + Data.State = Cell->GetCurrentState(); + } + return true; + }; + + auto ForEachWPFunction = [&ForEachCellFunction](const UWorldPartition* WorldPartition) -> bool + { + if (WorldPartition) + WorldPartition->RuntimeHash->ForEachStreamingCells(ForEachCellFunction); + return true; + }; + + WorldPartitionSubsystem->ForEachWorldPartition(ForEachWPFunction); +} + +// Finds the most specific WP cell that contains the given location. +// "Most specific" = smallest XY area (e.g. a house cell inside a landscape cell). +bool USpudRoamingActorSubsystem::FindCellForLocation( + const FVector& Location, + FString& OutCellName, + bool& OutIsActivated) const +{ + float SmallestArea = 0.f; + int32 BestIndex = INDEX_NONE; + + for (int32 i = 0; i < CellCache.Num(); ++i) + { + const FCachedCellData& Data = CellCache[i]; + + if (!Data.Bounds.IsInside(Location)) continue; + + const FVector Size = Data.Bounds.GetSize(); + const float Area = Size.X * Size.Y; + + if (BestIndex == INDEX_NONE || Area < SmallestArea) + { + SmallestArea = Area; + BestIndex = i; + } + } + + if (BestIndex == INDEX_NONE) + return false; + + OutCellName = CellCache[BestIndex].LevelName; + OutIsActivated = CellCache[BestIndex].State == EWorldPartitionRuntimeCellState::Activated; + return true; +} + +// Clamps the actor's location to within the bounds of the given cell (with a small inset). +// This ensures the actor restores strictly inside its cell and doesn't immediately +// trigger the unload logic due to a boundary position. +void USpudRoamingActorSubsystem::ClampActorToCell(AActor* Actor, const FString& CellName) const +{ + const FCachedCellData* TargetCell = CellCache.FindByPredicate([&CellName](const FCachedCellData& Data) + { + return Data.LevelName == CellName; + }); + + if (!TargetCell) return; + + // Small inset to avoid placing the actor exactly on the cell boundary + static constexpr float Inset = 10.f; + + const FVector Location = Actor->GetActorLocation(); + const FVector Clamped = FVector( + FMath::Clamp(Location.X, TargetCell->Bounds.Min.X + Inset, TargetCell->Bounds.Max.X - Inset), + FMath::Clamp(Location.Y, TargetCell->Bounds.Min.Y + Inset, TargetCell->Bounds.Max.Y - Inset), + FMath::Clamp(Location.Z, TargetCell->Bounds.Min.Z + Inset, TargetCell->Bounds.Max.Z - Inset) + ); + + if (!Clamped.Equals(Location)) + Actor->SetActorLocation(Clamped); +} + +// Clamps the actor into its target cell, stores it in SPUD, then queues it for destruction. +// Actual Destroy() is deferred to after the Tick loop to avoid invalidating iterators. +void USpudRoamingActorSubsystem::SaveAndDestroyActor( + FTrackedActor& Tracked, + const FString& CellName, + USpudSubsystem* Spud, + TArray& OutActorsToDestroy) +{ + ClampActorToCell(Tracked.Actor.Get(), CellName); + + if (Spud) + Spud->StoreActorByCell(Tracked.Actor.Get(), CellName); + + OutActorsToDestroy.Add(Tracked.Actor.Get()); +} + +void USpudRoamingActorSubsystem::Tick(float DeltaTime) +{ + // Only tick on the authority + if (!GetWorld()->GetAuthGameMode()) return; + + USpudSubsystem* Spud = CachedSpudSubsystem.Get(); + if (!Spud) return; + + // Collect actors to destroy after the loop to avoid iterator invalidation + TArray ActorsToDestroy; + + for (auto It = TrackedActors.CreateIterator(); It; ++It) + { + FTrackedActor& Tracked = *It; + + if (!Tracked.Actor.IsValid()) + { + It.RemoveCurrent(); + continue; + } + + FString CurrentCell; + bool bIsActivated = false; + const bool bFound = FindCellForLocation(Tracked.Actor->GetActorLocation(), CurrentCell, bIsActivated); + + if (bFound && bIsActivated) + { + // If Actor is in an active cell, update last known valid cell + Tracked.LastValidCellName = CurrentCell; + } + else if (bFound && !bIsActivated) + { + //If actor's cell is inactive, save into that cell and destroy + SaveAndDestroyActor(Tracked, CurrentCell, Spud, ActorsToDestroy); + } + else if (!bFound && !Tracked.LastValidCellName.IsEmpty()) + { + // IF actor is outside all cell bounds, fall back to last known cell + SaveAndDestroyActor(Tracked, Tracked.LastValidCellName, Spud, ActorsToDestroy); + } + else + { + //If no cell found and no fallback, cannot save, log a warning + UE_LOG(LogTemp, Warning, TEXT("RoamingActorSubsystem: %s has no valid cell, cannot save"), + *Tracked.Actor->GetName()); + } + } + + // Destroy queued actors — + for (AActor* Actor : ActorsToDestroy) + { + // Suppress network replication before destroying + //Actor->SetNetDormancy(DORM_DormantAll); + Actor->Destroy(); + } + +#if ENABLE_DRAW_DEBUG + // Visualize cell cache bounds + // Enable via console: RoamingActorSubsystem.DebugDrawCells 1 + if (CVarDebugDrawCells.GetValueOnGameThread()) + { + for (const FCachedCellData& Data : CellCache) + { + const FColor Color = Data.State == EWorldPartitionRuntimeCellState::Activated + ? FColor::Green + : FColor::Red; + + DrawDebugBox(GetWorld(), + Data.Bounds.GetCenter(), + Data.Bounds.GetExtent(), + Color, + false, + 0.f, + 0, + 50.f + ); + } + } +#endif +} \ No newline at end of file diff --git a/Source/SPUD/Private/SpudRuntimeStoredActorComponent.cpp b/Source/SPUD/Private/SpudRuntimeStoredActorComponent.cpp index d044a03..1b6b709 100644 --- a/Source/SPUD/Private/SpudRuntimeStoredActorComponent.cpp +++ b/Source/SPUD/Private/SpudRuntimeStoredActorComponent.cpp @@ -1,10 +1,6 @@ #include "SpudRuntimeStoredActorComponent.h" -#include "SpudSubsystem.h" -#include "WorldPartition/WorldPartition.h" -#include "WorldPartition/WorldPartitionRuntimeCell.h" -#include "WorldPartition/WorldPartitionRuntimeHash.h" -#include "WorldPartition/WorldPartitionSubsystem.h" +#include "SpudRoamingActorSubsystem.h" DEFINE_LOG_CATEGORY_STATIC(SpudRuntimeStoredActorComponent, All, All); @@ -17,151 +13,15 @@ USpudRuntimeStoredActorComponent::USpudRuntimeStoredActorComponent() void USpudRuntimeStoredActorComponent::BeginPlay() { Super::BeginPlay(); - - if (const auto SpudSubsystem = GetSpudSubsystem(GetWorld())) - { - // Register comp to subsystem tick check. - if (bCanCrossCell) - { - SpudSubsystem->RegisteredRuntimeStoredActorComponents.Add(this); - } - else - { - SpudSubsystem->OnLevelStore.AddDynamic(this, &ThisClass::OnLevelStore); - SpudSubsystem->PostUnloadStreamingLevel.AddDynamic(this, &ThisClass::OnPostUnloadCell); - SpudSubsystem->PreUnloadStreamingLevel.AddDynamic(this, &ThisClass::OnPreUnloadCell); - } - } + + if (USpudRoamingActorSubsystem* Tracker = GetWorld()->GetSubsystem()) + Tracker->RegisterActor(GetOwner()); } void USpudRuntimeStoredActorComponent::EndPlay(const EEndPlayReason::Type EndPlayReason) { - if (const auto SpudSubsystem = GetSpudSubsystem(GetWorld())) - { - // Unregister comp from subsystem tick check. - if (bCanCrossCell) - { - SpudSubsystem->RegisteredRuntimeStoredActorComponents.Remove(this); - } - else - { - SpudSubsystem->OnLevelStore.RemoveDynamic(this, &ThisClass::OnLevelStore); - SpudSubsystem->PostUnloadStreamingLevel.RemoveDynamic(this, &ThisClass::OnPostUnloadCell); - SpudSubsystem->PreUnloadStreamingLevel.RemoveDynamic(this, &ThisClass::OnPreUnloadCell); - } - } + if (USpudRoamingActorSubsystem* Tracker = GetWorld()->GetSubsystem()) + Tracker->UnregisterActor(GetOwner()); Super::EndPlay(EndPlayReason); -} - -void USpudRuntimeStoredActorComponent::UpdateCurrentCell(bool& CellActivated) -{ - const UWorldPartitionRuntimeCell* OutOverlappedCell = nullptr; - GetCurrentOverlappedCell(OutOverlappedCell); - - if (OutOverlappedCell) - { - CellActivated = OutOverlappedCell->GetCurrentState() == EWorldPartitionRuntimeCellState::Activated; - CurrentCellName = USpudState::GetLevelName(OutOverlappedCell); - } -} - -// ReSharper disable once CppMemberFunctionMayBeConst -void USpudRuntimeStoredActorComponent::OnLevelStore(const FString& LevelName) -{ - if (CurrentCellName.IsEmpty() || !IsActive()) - { - return; - } - - if (CurrentCellName == LevelName) - { - // Always store when cell unloaded - const auto Actor = GetOwner(); - UE_LOG(SpudRuntimeStoredActorComponent, Log, TEXT("Storing actor in cell: %s"), *CurrentCellName); - GetSpudSubsystem(GetWorld())->StoreActorByCell(Actor, CurrentCellName); - } -} - -// ReSharper disable once CppMemberFunctionMayBeConst -void USpudRuntimeStoredActorComponent::OnPostUnloadCell(const FName& LevelName) -{ - if (CurrentCellName.IsEmpty() || !IsActive()) - { - return; - } - - if (CurrentCellName == LevelName) - { - DestroyActor(); - } -} - -void USpudRuntimeStoredActorComponent::OnPreUnloadCell(const FName& LevelName) -{ - // Fixed: Only for can't cross cell actor(static actor) and only update when current cell is null. - bool bCellActivated; - UpdateCurrentCell(bCellActivated); -} - -void USpudRuntimeStoredActorComponent::GetCurrentOverlappedCell( - const UWorldPartitionRuntimeCell*& CurrentOverlappedCell) const -{ - const auto WorldPartitionSubsystem = GetWorld()->GetSubsystem(); - if (!WorldPartitionSubsystem) - { - return; - } - - const auto OwnerLocation = GetOwner()->GetActorLocation(); - auto SmallestCellVolume = 0.f; - - const auto ForEachCellFunction = [this, &CurrentOverlappedCell, &OwnerLocation, &SmallestCellVolume](const UWorldPartitionRuntimeCell* Cell) -> bool - { - // for simplicity, assuming actor bounds are small enough that only a single cell needs to be considered - if (const auto CellBounds = Cell->GetCellBounds(); CellBounds.IsInsideXY(OwnerLocation)) - { - // use the smallest cell - if (const auto Volume = CellBounds.GetVolume(); !CurrentOverlappedCell || Volume < SmallestCellVolume) - { - // we dont need this log - //UE_LOG(SpudRuntimeStoredActorComponent, Log, TEXT("GetCurrentOverlappedCell: found cell %s"), *Cell->GetName()); - - SmallestCellVolume = Volume; - CurrentOverlappedCell = Cell; - } - } - return true; - }; - - // ReSharper disable once CppParameterMayBeConstPtrOrRef - auto ForEachWpFunction = [ForEachCellFunction](UWorldPartition* WorldPartition) -> bool - { - if (WorldPartition) - { - WorldPartition->RuntimeHash->ForEachStreamingCells(ForEachCellFunction); - } - - return true; - }; - - WorldPartitionSubsystem->ForEachWorldPartition(ForEachWpFunction); -} - -// ReSharper disable once CppMemberFunctionMayBeConst -void USpudRuntimeStoredActorComponent::DestroyActor() -{ - // If this is pawn, we also destroy its controller (AI) - if (auto Pawn = Cast(GetOwner())) - { - auto Controller = Pawn->GetController(); - if (Controller && Pawn->IsBotControlled()) - { - UE_LOG(SpudRuntimeStoredActorComponent, Log, TEXT("Destroying actor's controller in cell: %s"), *CurrentCellName); - Controller->Destroy(); - } - } - - UE_LOG(SpudRuntimeStoredActorComponent, Log, TEXT("Destroying actor in cell: %s"), *CurrentCellName); - GetOwner()->Destroy(); -} +} \ No newline at end of file diff --git a/Source/SPUD/Private/SpudState.cpp b/Source/SPUD/Private/SpudState.cpp index a0cc293..b46a262 100644 --- a/Source/SPUD/Private/SpudState.cpp +++ b/Source/SPUD/Private/SpudState.cpp @@ -59,6 +59,12 @@ void USpudState::StoreLevel(ULevel* Level, bool bReleaseAfter, bool bBlocking) { if (SpudPropertyUtil::IsPersistentObject(Actor)) { + // Skip RoamingActors + if (Actor->Implements()) + { + continue; + } + StoreActor(Actor, LevelData); } } @@ -455,6 +461,7 @@ void USpudState::StoreObjectProperties(UObject* Obj, uint32 PrefixID, TArrayGetLevels()) @@ -476,73 +483,112 @@ void USpudState::RestoreLevel(UWorld* World, const FString& LevelName) void USpudState::RestoreLevel(ULevel* Level) { - if (!IsValid(Level)) - return; - - FString LevelName = GetLevelName(Level); - auto LevelData = GetLevelData(LevelName, false); + if (!IsValid(Level)) + return; - if (!LevelData.IsValid()) - { - UE_LOG(LogSpudState, Log, TEXT("Skipping restore level %s, no data (this may be fine)"), *LevelName); - return; - } + const FString LevelName = GetLevelName(Level); + const auto LevelData = GetLevelData(LevelName, false); + if (!LevelData.IsValid()) + { + UE_LOG(LogSpudState, Log, TEXT("Skipping restore level %s, no data (this may be fine)"), *LevelName); + return; + } + // Mutex lock the level (load and unload events on streaming can be in loading threads) - FScopeLock LevelLock(&LevelData->Mutex); + FScopeLock LevelLock(&LevelData->Mutex); + UE_LOG(LogSpudState, Verbose, TEXT("RESTORE level %s - Start"), *LevelName); + + TMap PersistentObjectsByGuid; + TArray LevelActorsToRestore; + TArray RoamingActorsToRestore; + + // Collect persistent actors and pre-populate guid map + /* + Dynamically spawned actors can be retained in a level during streaming, + until the level's data is collected by the GC: + https://dev.epicgames.com/community/learning/knowledge-base/r6wl/unreal-engine-world-building-guide#wp-limitations + + This means when a level is reloaded before GC runs, both the old and new instances + of a spawned actor can exist simultaneously on the level. To avoid respawning + duplicates during RestoreLevel, we pre-populate PersistentObjectsByGuid with + already-existing actors. If a SpawnedActor entry has a GUID that's already in the map, + we skip respawning it, the existing instance will be restored instead. + */ + for (AActor* Actor : Level->Actors) + { + if (!Actor) continue; + if (!SpudPropertyUtil::IsPersistentObject(Actor)) continue; + + LevelActorsToRestore.Add(Actor); + + const FGuid Guid = SpudPropertyUtil::GetGuidProperty(Actor); + if (Guid.IsValid()) + PersistentObjectsByGuid.Add(Guid, Actor); + } - UE_LOG(LogSpudState, Verbose, TEXT("RESTORE level %s - Start"), *LevelName); - TMap RuntimeObjectsByGuid; - // Respawn dynamic actors first; they need to exist in order for cross-references in level actors to work - for (auto&& SpawnedActor : LevelData->SpawnedActors.Contents) - { - auto Actor = RespawnActor(SpawnedActor.Value, LevelData->Metadata, Level); - if (Actor) - RuntimeObjectsByGuid.Add(SpawnedActor.Value.Guid, Actor); - // Spawned actors will have been added to Level->Actors, their state will be restored there - } + // Respawn dynamic actors first so cross-references resolve correctly + // Skip if actor already exists on level (e.g. WP cell reloaded before GC collected old actors) + for (auto&& SpawnedActor : LevelData->SpawnedActors.Contents) + { + const FGuid& Guid = SpawnedActor.Value.Guid; + + if (UObject** Existing = PersistentObjectsByGuid.Find(Guid)) + { +#if WITH_EDITOR + if (AActor* ExistingActor = Cast(*Existing)) + { + if (ExistingActor->Implements()) + { + UE_LOG(LogSpudState, Warning, + TEXT("RESTORE level %s - roaming actor %s already exists, something went wrong"), + *LevelName, *ExistingActor->GetName()); + } + } +#endif + continue; + } - TMap RestoredRuntimeActors; + auto Actor = RespawnActor(SpawnedActor.Value, LevelData->Metadata, Level); + if (Actor) + { + if (Actor->Implements()) + RoamingActorsToRestore.Add(Actor); + else + LevelActorsToRestore.Add(Actor); - // Restore existing actor state - for (auto Actor : Level->Actors) - { - if (SpudPropertyUtil::IsPersistentObject(Actor)) - { - RestoreActor(Actor, LevelData, &RuntimeObjectsByGuid); - auto Guid = SpudPropertyUtil::GetGuidProperty(Actor); - if (Guid.IsValid()) - { - if (RuntimeObjectsByGuid.Contains(Guid)) - { - if (const auto DuplicatedActor = RestoredRuntimeActors.Find(Guid)) - { - UE_LOG(LogSpudState, Verbose, TEXT("RESTORE level %s - destroying duplicate runtime actor %s"), - *LevelName, *Guid.ToString(EGuidFormats::DigitsWithHyphens)); + PersistentObjectsByGuid.Add(Guid, Actor); + } + } - // sometimes runtime actors are duplicated in the level actors array - for example, when hiding a - // world partition cell and immediately showing it; need to remove duplicates in this case - (*DuplicatedActor)->Destroy(); - } - else - { - RestoredRuntimeActors.Emplace(Guid, Actor); - } - } - else - { - RuntimeObjectsByGuid.Add(Guid, Actor); - } - } - } - } - // Destroy actors in level but missing from save state - for (auto&& DestroyedActor : LevelData->DestroyedActors.Values) - { - DestroyActor(*DestroyedActor, Level); - } - UE_LOG(LogSpudState, Verbose, TEXT("RESTORE level %s - Complete"), *LevelName); + // Restore all non-roaming actors + for (AActor* Actor : LevelActorsToRestore) + { + //Skip RoamingActors restoration by PersistentLevel + if (Actor->Implements()) continue; + + RestoreActor(Actor, LevelData, &PersistentObjectsByGuid); + const FGuid Guid = SpudPropertyUtil::GetGuidProperty(Actor); + if (Guid.IsValid() && !PersistentObjectsByGuid.Contains(Guid)) + PersistentObjectsByGuid.Add(Guid, Actor); + } + + // Restore roaming actors + for (AActor* Actor : RoamingActorsToRestore) + { + RestoreActor(Actor, LevelData, &PersistentObjectsByGuid); + + const FGuid Guid = SpudPropertyUtil::GetGuidProperty(Actor); + if (Guid.IsValid() && !PersistentObjectsByGuid.Contains(Guid)) + PersistentObjectsByGuid.Add(Guid, Actor); + } + + // Destroy actors missing from save state + for (auto&& DestroyedActor : LevelData->DestroyedActors.Values) + DestroyActor(*DestroyedActor, Level); + + UE_LOG(LogSpudState, Verbose, TEXT("RESTORE level %s - Complete"), *LevelName); } bool USpudState::PreLoadLevelData(const FString& LevelName) @@ -583,8 +629,19 @@ AActor* USpudState::RespawnActor(const FSpudSpawnedActorData& SpawnedActor, UE_LOG(LogSpudState, Error, TEXT("Cannot respawn instance of %s, class not found"), *ClassName); return nullptr; } + + FActorSpawnParameters Params; - Params.OverrideLevel = Level; + + if (Class->ImplementsInterface(USpudRoamingActor::StaticClass())) + { + Params.OverrideLevel = Level->GetWorld()->PersistentLevel; + } + else + { + Params.OverrideLevel = Level; + } + // Need to always spawn since we're not setting position until later Params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; UE_LOG(LogSpudState, Verbose, TEXT(" * Respawning actor %s of type %s"), *SpawnedActor.Guid.ToString(EGuidFormats::DigitsWithHyphens), *ClassName); @@ -1415,11 +1472,16 @@ FString USpudState::GetLevelName(const UWorldPartitionRuntimeCell* Cell) { if (!Cell) return ""; - - FString LevelName = Cell->GetWorld()->GetMapName() + "_" + Cell->GetName(); + + FString LevelName = Cell->GetName(); + //For builds, cell name will already match what SPUD uses for WP streaming processing +#if WITH_EDITOR + LevelName = Cell->GetWorld()->GetMapName() + "_" + LevelName; // Strip off PIE prefix, "UEDPIE_N_" where N is a number if (LevelName.StartsWith("UEDPIE_")) LevelName = LevelName.Right(LevelName.Len() - 9); +#endif + return LevelName; } diff --git a/Source/SPUD/Private/SpudSubsystem.cpp b/Source/SPUD/Private/SpudSubsystem.cpp index b85b9d1..d55c4d2 100644 --- a/Source/SPUD/Private/SpudSubsystem.cpp +++ b/Source/SPUD/Private/SpudSubsystem.cpp @@ -4,7 +4,6 @@ #include "Engine/LocalPlayer.h" #include "Kismet/GameplayStatics.h" #include "ImageUtils.h" -#include "SpudRuntimeStoredActorComponent.h" #include "TimerManager.h" #include "HAL/FileManager.h" #include "Async/Async.h" @@ -1002,27 +1001,6 @@ void USpudSubsystem::UnloadStreamLevel(FName LevelName) } } -void USpudSubsystem::UpdateRegisteredComps() -{ - // Ticking registered comp's owner is moving outside the loaded area. - TArray NeedToDestroyArray; - for (const auto RegComp : RegisteredRuntimeStoredActorComponents) - { - bool bCellActivated; - RegComp->UpdateCurrentCell(bCellActivated); - if (!bCellActivated) - { - NeedToDestroyArray.Add(RegComp); - } - } - - for (auto Destroy : NeedToDestroyArray) - { - StoreActorByCell(Destroy->GetOwner(), Destroy->CurrentCellName); - Destroy->DestroyActor(); - } -} - void USpudSubsystem::ForceReset() { CurrentState = ESpudSystemState::RunningIdle; @@ -1089,6 +1067,9 @@ UTexture2D* USpudSubsystem::GetRenderTargetData(FString Name) void USpudSubsystem::StoreActorByCell(AActor* Actor, const FString& CellName) { + if (!SpudPropertyUtil::IsPersistentObject(Actor)) + return; + GetActiveState()->StoreActor(Actor, CellName); } @@ -1587,8 +1568,6 @@ void USpudSubsystem::Tick(float DeltaTime) PostUnloadStreamingLevel.Broadcast(FName(USpudState::GetLevelName(Level->GetWorldAssetPackageName()))); } } - - UpdateRegisteredComps(); } } } diff --git a/Source/SPUD/Public/ISpudObject.h b/Source/SPUD/Public/ISpudObject.h index 0716e1c..2421795 100644 --- a/Source/SPUD/Public/ISpudObject.h +++ b/Source/SPUD/Public/ISpudObject.h @@ -142,3 +142,20 @@ class SPUD_API ISpudObjectCallback void SpudPostRestore(const USpudState* State); }; + +UINTERFACE(MinimalAPI) +class USpudRoamingActor : public UInterface +{ + GENERATED_BODY() +}; + +/** +* Opts an actor into the RoamingActorSubsystem. +* Implement this interface on actors that can move between World Partition cells. +* The tracker subsystem will automatically manage saving and destroying them +* when they leave loaded cells. +*/ +class SPUD_API ISpudRoamingActor +{ + GENERATED_BODY() +}; \ No newline at end of file diff --git a/Source/SPUD/Public/SpudRoamingActorSubsystem.h b/Source/SPUD/Public/SpudRoamingActorSubsystem.h new file mode 100644 index 0000000..7da452b --- /dev/null +++ b/Source/SPUD/Public/SpudRoamingActorSubsystem.h @@ -0,0 +1,72 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Subsystems/WorldSubsystem.h" +#include "WorldPartition/WorldPartitionLevelStreamingPolicy.h" +#include "SpudRoamingActorSubsystem.generated.h" + +class USpudSubsystem; + +UCLASS() +class SPUD_API USpudRoamingActorSubsystem : public UTickableWorldSubsystem +{ + GENERATED_BODY() + +public: + UFUNCTION(BlueprintCallable, Category = "RoamingActorSubsystem") + void RegisterActor(AActor* Actor); + + UFUNCTION(BlueprintCallable, Category = "RoamingActorSubsystem") + void UnregisterActor(AActor* Actor); + +protected: + virtual void Initialize(FSubsystemCollectionBase& Collection) override; + virtual void Deinitialize() override; + virtual bool ShouldCreateSubsystem(UObject* Outer) const override; + + virtual TStatId GetStatId() const override + { + RETURN_QUICK_DECLARE_CYCLE_STAT(USpudRoamingActorSubsystem , STATGROUP_Tickables); + } + + virtual void Tick(float DeltaTime) override; + virtual bool IsTickableWhenPaused() const override { return false; } + + UFUNCTION() + void OnLevelStore(const FString& LevelName); + + UFUNCTION() + void OnPostLoadStreamingLevel(const FName& LevelName); + + UFUNCTION() + void OnPostUnloadStreamingLevel(const FName& LevelName); + + UFUNCTION() + void OnPreUnloadStreamingLevel(const FName& LevelName); + +private: + struct FTrackedActor + { + TWeakObjectPtr Actor; + FString LastValidCellName; + }; + + struct FCachedCellData + { + const UWorldPartitionRuntimeCell* Cell; + FBox Bounds; + FString LevelName; + EWorldPartitionRuntimeCellState State; + bool bPendingUnload = false; + }; + + TArray TrackedActors; + TArray CellCache; + TWeakObjectPtr CachedSpudSubsystem; + + void OnStreamingStateUpdated(); + void RebuildCellCache(); + bool FindCellForLocation(const FVector& Location, FString& OutCellName, bool& OutIsActivated) const; + void ClampActorToCell(AActor* Actor, const FString& CellName) const; + void SaveAndDestroyActor(FTrackedActor& Tracked, const FString& CellName, USpudSubsystem* Spud, TArray& OutActorsToDestroy); +}; \ No newline at end of file diff --git a/Source/SPUD/Public/SpudRuntimeStoredActorComponent.h b/Source/SPUD/Public/SpudRuntimeStoredActorComponent.h index ce5b24e..4ac3b46 100644 --- a/Source/SPUD/Public/SpudRuntimeStoredActorComponent.h +++ b/Source/SPUD/Public/SpudRuntimeStoredActorComponent.h @@ -21,27 +21,7 @@ class SPUD_API USpudRuntimeStoredActorComponent : public UActorComponent * Can the owning actor can cross cells? Ticking will be enabled if this flag is true. If the position changes * infrequently, it's more efficient to manually call UpdateCurrentCell(). */ - UPROPERTY(EditDefaultsOnly) - bool bCanCrossCell = false; - - FString CurrentCellName; - - void UpdateCurrentCell(bool& CellActivated); - void DestroyActor(); - protected: virtual void BeginPlay() override; virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; - -private: - UFUNCTION() - void OnLevelStore(const FString& LevelName); - - UFUNCTION() - void OnPostUnloadCell(const FName& LevelName); - - UFUNCTION() - void OnPreUnloadCell(const FName& LevelName); - - void GetCurrentOverlappedCell(const UWorldPartitionRuntimeCell*& CurrentOverlappedCell) const; }; diff --git a/Source/SPUD/Public/SpudSubsystem.h b/Source/SPUD/Public/SpudSubsystem.h index ab96f55..774aa39 100644 --- a/Source/SPUD/Public/SpudSubsystem.h +++ b/Source/SPUD/Public/SpudSubsystem.h @@ -10,7 +10,6 @@ #include "SpudSubsystem.generated.h" -class USpudRuntimeStoredActorComponent; DECLARE_LOG_CATEGORY_EXTERN(LogSpudSubsystem, Verbose, Verbose); DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FSpudPreLoadGame, const FString&, SlotName); @@ -164,9 +163,6 @@ class SPUD_API USpudSubsystem : public UGameInstanceSubsystem, public FTickableG UPROPERTY(BlueprintReadWrite, Config) TArray ExcludeLevelNamePatterns; - UPROPERTY(BlueprintReadOnly) - TSet> RegisteredRuntimeStoredActorComponents; - protected: FDelegateHandle OnPreLoadMapHandle; FDelegateHandle OnPostLoadMapHandle;