Add LoadSceneAdditive / UnloadSceneAdditive for briefing scene preload

Lets a briefing scene materialize only the preload-tagged actors from a
scenario .pss (player placement markers, briefing props) without
clearing the current world, pushing timeline data, registering ActorIds,
or starting any auto-play. The actors spawned by each LoadSceneAdditive
call are tracked in a static TWeakObjectPtr array; UnloadSceneAdditive
destroys every still-valid one and clears the array.

Typical BP usage on scenario change:
  UnloadSceneAdditive(this) -> LoadSceneAdditive(this, NewSceneName)

No BP-side bookkeeping required. Filter defaults to PreloadOnly which
matches the intended briefing use case; the parameter stays exposed for
other niche cases (ExcludePreload, All).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-20 15:10:52 +02:00
parent 34280de44a
commit f6efc00c73
2 changed files with 127 additions and 0 deletions

View File

@@ -124,6 +124,102 @@ bool UPS_Editor_SceneLoader::LoadScene(const UObject* WorldContextObject, const
return true; return true;
} }
TArray<TWeakObjectPtr<AActor>> UPS_Editor_SceneLoader::AdditiveSpawnedActors;
bool UPS_Editor_SceneLoader::LoadSceneAdditive(const UObject* WorldContextObject, const FString& SceneName, EPS_Editor_SceneLoadFilter Filter, bool bStripEditorComponents)
{
if (!WorldContextObject || SceneName.IsEmpty())
{
return false;
}
UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull);
if (!World)
{
return false;
}
// Read + parse the scenario JSON exactly like LoadScene
const FString FilePath = GetSceneFilePath(SceneName);
FString JsonString;
if (!FFileHelper::LoadFileToString(JsonString, *FilePath))
{
UE_LOG(LogTemp, Error, TEXT("PS_Editor SceneLoader (Additive): Failed to read file: %s"), *FilePath);
return false;
}
FPS_Editor_SceneData SceneData;
if (!FJsonObjectConverter::JsonObjectStringToUStruct(JsonString, &SceneData, 0, 0))
{
UE_LOG(LogTemp, Error, TEXT("PS_Editor SceneLoader (Additive): Failed to parse JSON from: %s"), *FilePath);
return false;
}
// Spawn matching actors WITHOUT touching the timeline subsystem, registering ActorIds,
// or starting any auto-play. The briefing scene drives its own timeline.
int32 SpawnedCount = 0;
bIsCurrentlyLoading = true;
for (const FPS_Editor_ActorData& ActorData : SceneData.Actors)
{
AActor* SpawnedActor = SpawnActorFromData(World, ActorData, Filter);
if (SpawnedActor)
{
AdditiveSpawnedActors.Add(SpawnedActor);
if (bStripEditorComponents)
{
TArray<UActorComponent*> ToRemove;
if (UActorComponent* Spawnable = SpawnedActor->FindComponentByClass<UPS_Editor_SpawnableComponent>())
{
ToRemove.Add(Spawnable);
}
if (UActorComponent* Editable = SpawnedActor->FindComponentByClass<UPS_Editor_EditableComponent>())
{
ToRemove.Add(Editable);
}
for (UActorComponent* Comp : ToRemove)
{
Comp->DestroyComponent();
}
if (IPS_Editor_SplineEditable* Spline = Cast<IPS_Editor_SplineEditable>(SpawnedActor))
{
Spline->SetVisualizationVisible(false);
}
}
SpawnedCount++;
}
}
bIsCurrentlyLoading = false;
UE_LOG(LogTemp, Log, TEXT("PS_Editor SceneLoader (Additive): Spawned %d actor(s) from '%s' [filter=%d]"),
SpawnedCount, *SceneName, (int32)Filter);
return SpawnedCount > 0;
}
int32 UPS_Editor_SceneLoader::UnloadSceneAdditive(const UObject* WorldContextObject)
{
int32 Destroyed = 0;
for (TWeakObjectPtr<AActor>& Weak : AdditiveSpawnedActors)
{
if (AActor* Actor = Weak.Get())
{
Actor->Destroy();
++Destroyed;
}
// else: already gone (GC, manual destroy, level reload) — skip silently
}
AdditiveSpawnedActors.Reset();
if (Destroyed > 0)
{
UE_LOG(LogTemp, Log, TEXT("PS_Editor SceneLoader (Additive): Unloaded %d actor(s)"), Destroyed);
}
return Destroyed;
}
bool UPS_Editor_SceneLoader::LoadSceneFromData(UWorld* World, const FPS_Editor_SceneData& SceneData, TArray<AActor*>& OutActors, EPS_Editor_SceneLoadFilter Filter) bool UPS_Editor_SceneLoader::LoadSceneFromData(UWorld* World, const FPS_Editor_SceneData& SceneData, TArray<AActor*>& OutActors, EPS_Editor_SceneLoadFilter Filter)
{ {
if (!World) if (!World)

View File

@@ -56,6 +56,33 @@ public:
static bool LoadSceneFromData(UWorld* World, const FPS_Editor_SceneData& SceneData, TArray<AActor*>& OutActors, static bool LoadSceneFromData(UWorld* World, const FPS_Editor_SceneData& SceneData, TArray<AActor*>& OutActors,
EPS_Editor_SceneLoadFilter Filter = EPS_Editor_SceneLoadFilter::All); EPS_Editor_SceneLoadFilter Filter = EPS_Editor_SceneLoadFilter::All);
/**
* Additive load: spawns actors from a scenario WITHOUT clearing the current world and
* WITHOUT touching the timeline subsystem. Designed for briefing scenes where you only
* want to materialize preload-tagged actors (player placement markers, briefing props)
* configured in the main scenario.
*
* Spawned actors are tracked internally — call UnloadSceneAdditive() to destroy them all
* before loading the next scenario.
*
* @param SceneName Scenario name (without extension)
* @param Filter Which actors to spawn — defaults to PreloadOnly
* @param bStripEditorComponents If true, removes SpawnableComponent / EditableComponent after spawn
* @return True if at least one actor was spawned
*/
UFUNCTION(BlueprintCallable, Category = "PS_Editor", meta = (WorldContext = "WorldContextObject"))
static bool LoadSceneAdditive(const UObject* WorldContextObject, const FString& SceneName,
EPS_Editor_SceneLoadFilter Filter = EPS_Editor_SceneLoadFilter::PreloadOnly,
bool bStripEditorComponents = true);
/**
* Destroy every actor previously spawned via LoadSceneAdditive. No-op if nothing was loaded.
* Call this before LoadSceneAdditive when switching briefing scenarios.
* @return Number of actors destroyed.
*/
UFUNCTION(BlueprintCallable, Category = "PS_Editor", meta = (WorldContext = "WorldContextObject"))
static int32 UnloadSceneAdditive(const UObject* WorldContextObject);
/** /**
* Get the directory where scenes are stored. * Get the directory where scenes are stored.
*/ */
@@ -114,4 +141,8 @@ private:
/** Apply custom editable properties to a spawned actor. */ /** Apply custom editable properties to a spawned actor. */
static void ApplyCustomProperties(AActor* Actor, const TMap<FString, FString>& CustomProperties); static void ApplyCustomProperties(AActor* Actor, const TMap<FString, FString>& CustomProperties);
/** Actors spawned via LoadSceneAdditive, tracked so UnloadSceneAdditive can find them.
* TWeakObjectPtr handles GC / manual destruction gracefully (skipped at unload time). */
static TArray<TWeakObjectPtr<AActor>> AdditiveSpawnedActors;
}; };