Add ChildMovable interface, preload filter, IsInEditorMode, lightmap init

- Add IPS_Editor_ChildMovable interface for per-child editing of sub-elements
  (VR placement spots, etc.) with full gizmo, undo, and serialization support
- Add "Edit Children" button in toolbar when a ChildMovable actor is selected
- Add bPreload tag on SpawnableComponent for briefing/placement phase actors
- Add EPS_Editor_SceneLoadFilter enum (All, PreloadOnly, ExcludePreload) to
  LoadScene for selective loading of preload vs gameplay actors
- Add bPreload field to FPS_Editor_ActorData for JSON persistence
- Add static IsInEditorMode() getter on SceneLoader (BlueprintPure)
- Add Level::InitializeRenderingResources() call on sublevel load for lightmaps
- Update Pawn, PlayerController, UndoManager, SceneSerializer, SceneLoader,
  MainWidget to support ChildMovable alongside existing SplineEditable

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-15 18:02:17 +02:00
parent 5859d3bf6a
commit 1a2cc87160
11 changed files with 304 additions and 22 deletions

View File

@@ -15,6 +15,7 @@
#include "PS_Editor_UndoManager.h" #include "PS_Editor_UndoManager.h"
#include "PS_Editor_SplineActor.h" #include "PS_Editor_SplineActor.h"
#include "PS_Editor_SplineEditable.h" #include "PS_Editor_SplineEditable.h"
#include "PS_Editor_ChildMovable.h"
#include "Components/SplineComponent.h" #include "Components/SplineComponent.h"
#include "DrawDebugHelpers.h" #include "DrawDebugHelpers.h"
#include "PS_Editor_ConfirmDialog.h" #include "PS_Editor_ConfirmDialog.h"
@@ -380,16 +381,21 @@ void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value)
} }
const FVector ConstrainedDelta = DisplacementAlongAxis * AxisDir; const FVector ConstrainedDelta = DisplacementAlongAxis * AxisDir;
// Spline point mode: move the single point instead of actors // Spline point / ChildMovable mode: move the single sub-element instead of actors
if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid()) if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid())
{ {
if (IPS_Editor_SplineEditable* SplineActor = Cast<IPS_Editor_SplineEditable>(DraggedSplineActor.Get())) if (SplinePointDragInitialState.IsValidIndex(ActiveSplinePointIndex))
{ {
if (SplinePointDragInitialState.IsValidIndex(ActiveSplinePointIndex)) const FVector NewPos = SplinePointDragInitialState[ActiveSplinePointIndex] + ConstrainedDelta;
if (IPS_Editor_SplineEditable* SplineActor = Cast<IPS_Editor_SplineEditable>(DraggedSplineActor.Get()))
{ {
const FVector NewPos = SplinePointDragInitialState[ActiveSplinePointIndex] + ConstrainedDelta;
SplineActor->MovePoint(ActiveSplinePointIndex, NewPos); SplineActor->MovePoint(ActiveSplinePointIndex, NewPos);
} }
else if (DraggedSplineActor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass()))
{
IPS_Editor_ChildMovable::Execute_MoveChild(DraggedSplineActor.Get(), ActiveSplinePointIndex, NewPos);
}
} }
} }
else else
@@ -475,11 +481,15 @@ void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value)
{ {
if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid()) if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid())
{ {
// Following a single spline point // Following a single spline point or child movable element
if (IPS_Editor_SplineEditable* SplineActor = Cast<IPS_Editor_SplineEditable>(DraggedSplineActor.Get())) if (IPS_Editor_SplineEditable* SplineActor = Cast<IPS_Editor_SplineEditable>(DraggedSplineActor.Get()))
{ {
Gizmo->SetActorLocation(SplineActor->GetPointLocation(ActiveSplinePointIndex)); Gizmo->SetActorLocation(SplineActor->GetPointLocation(ActiveSplinePointIndex));
} }
else if (DraggedSplineActor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass()))
{
Gizmo->SetActorLocation(IPS_Editor_ChildMovable::Execute_GetChildLocation(DraggedSplineActor.Get(), ActiveSplinePointIndex));
}
} }
else if (Selected.Num() == 1) else if (Selected.Num() == 1)
{ {
@@ -648,6 +658,35 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
} }
} }
// Check for ChildMovable child handle hit
if (APS_Editor_PlayerController* EdPC_CM = Cast<APS_Editor_PlayerController>(GetController()))
{
if (EdPC_CM->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement)
{
AActor* ActiveActor = EdPC_CM->GetActivePlacementActor();
if (ActiveActor && ActiveActor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass()))
{
const int32 ChildIdx = IPS_Editor_ChildMovable::Execute_GetChildIndexUnderCursor(ActiveActor, RayOrigin, RayDir);
if (ChildIdx >= 0)
{
ActiveSplinePointIndex = ChildIdx;
DraggedSplineActor = ActiveActor;
IPS_Editor_ChildMovable::Execute_HighlightChild(ActiveActor, ChildIdx);
if (UPS_Editor_SelectionManager* SM = EdPC_CM->GetSelectionManager())
{
if (APS_Editor_Gizmo* Gizmo = SM->GetGizmo())
{
Gizmo->SetGizmoActive(true, IPS_Editor_ChildMovable::Execute_GetChildLocation(ActiveActor, ChildIdx));
}
SM->SetTransformMode(EPS_Editor_TransformMode::Translate);
}
return;
}
}
}
}
// In SplinePlacement mode: click on spline tube mesh → insert a CP // In SplinePlacement mode: click on spline tube mesh → insert a CP
if (APS_Editor_PlayerController* EdPC2 = Cast<APS_Editor_PlayerController>(GetController())) if (APS_Editor_PlayerController* EdPC2 = Cast<APS_Editor_PlayerController>(GetController()))
{ {
@@ -804,13 +843,27 @@ void APS_Editor_Pawn::HandleLeftClickCompleted(const FInputActionValue& Value)
// Spline point drag: record SplinePointAction // Spline point drag: record SplinePointAction
if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid()) if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid())
{ {
TArray<FVector> NewPoints;
FString Desc;
if (IPS_Editor_SplineEditable* SplineActor = Cast<IPS_Editor_SplineEditable>(DraggedSplineActor.Get())) if (IPS_Editor_SplineEditable* SplineActor = Cast<IPS_Editor_SplineEditable>(DraggedSplineActor.Get()))
{
NewPoints = SplineActor->GetAllPointLocations();
Desc = TEXT("Move spline point");
}
else if (DraggedSplineActor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass()))
{
NewPoints = IPS_Editor_ChildMovable::Execute_GetAllChildLocations(DraggedSplineActor.Get());
Desc = TEXT("Move child element");
}
if (NewPoints.Num() > 0)
{ {
TSharedPtr<FPS_Editor_SplinePointAction> Action = MakeShared<FPS_Editor_SplinePointAction>(); TSharedPtr<FPS_Editor_SplinePointAction> Action = MakeShared<FPS_Editor_SplinePointAction>();
Action->SplineActor = Cast<AActor>(SplineActor); Action->SplineActor = DraggedSplineActor.Get();
Action->OldPoints = SplinePointDragInitialState; Action->OldPoints = SplinePointDragInitialState;
Action->NewPoints = SplineActor->GetAllPointLocations(); Action->NewPoints = NewPoints;
Action->Description = TEXT("Move spline point"); Action->Description = Desc;
UM->RecordAction(Action); UM->RecordAction(Action);
} }
} }
@@ -1439,13 +1492,17 @@ void APS_Editor_Pawn::BeginGizmoDrag(EPS_Editor_GizmoAxis Axis)
} }
} }
// Store initial spline state if editing a point // Store initial sub-element state if editing a point/child
if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid()) if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid())
{ {
if (IPS_Editor_SplineEditable* Spline = Cast<IPS_Editor_SplineEditable>(DraggedSplineActor.Get())) if (IPS_Editor_SplineEditable* Spline = Cast<IPS_Editor_SplineEditable>(DraggedSplineActor.Get()))
{ {
SplinePointDragInitialState = Spline->GetAllPointLocations(); SplinePointDragInitialState = Spline->GetAllPointLocations();
} }
else if (DraggedSplineActor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass()))
{
SplinePointDragInitialState = IPS_Editor_ChildMovable::Execute_GetAllChildLocations(DraggedSplineActor.Get());
}
} }
SetCameraMode(EPS_Editor_CameraMode::GizmoDrag); SetCameraMode(EPS_Editor_CameraMode::GizmoDrag);

View File

@@ -12,6 +12,8 @@
#include "Engine/LevelStreamingDynamic.h" #include "Engine/LevelStreamingDynamic.h"
#include "Engine/PostProcessVolume.h" #include "Engine/PostProcessVolume.h"
#include "PS_Editor_CameraStart.h" #include "PS_Editor_CameraStart.h"
#include "PS_Editor_ChildMovable.h"
#include "PS_Editor_SceneLoader.h"
#include "PS_Editor_HUD.h" #include "PS_Editor_HUD.h"
#include "PS_Editor_MainWidget.h" #include "PS_Editor_MainWidget.h"
#include "Camera/PlayerCameraManager.h" #include "Camera/PlayerCameraManager.h"
@@ -32,6 +34,7 @@ void APS_Editor_PlayerController::BeginPlay()
{ {
Super::BeginPlay(); Super::BeginPlay();
UPS_Editor_SceneLoader::bIsEditorModeActive = true;
SetInputMode(FInputModeGameAndUI().SetHideCursorDuringCapture(false)); SetInputMode(FInputModeGameAndUI().SetHideCursorDuringCapture(false));
// AI logic will be stopped per-pawn when spawned (see SpawnManager) // AI logic will be stopped per-pawn when spawned (see SpawnManager)
@@ -190,6 +193,7 @@ void APS_Editor_PlayerController::CloseEditor()
} }
// Notify actors: leaving editor mode // Notify actors: leaving editor mode
UPS_Editor_SceneLoader::bIsEditorModeActive = false;
if (SpawnManager) if (SpawnManager)
{ {
SpawnManager->NotifyEditorModeChanged(false); SpawnManager->NotifyEditorModeChanged(false);
@@ -303,20 +307,29 @@ void APS_Editor_PlayerController::BeginPointPlacement(TSubclassOf<AActor> InActo
void APS_Editor_PlayerController::BeginPointPlacementAppend(AActor* ExistingActor) void APS_Editor_PlayerController::BeginPointPlacementAppend(AActor* ExistingActor)
{ {
if (!ExistingActor || !ExistingActor->Implements<UPS_Editor_PointPlaceable>()) return; if (!ExistingActor) return;
const bool bIsPointPlaceable = ExistingActor->Implements<UPS_Editor_PointPlaceable>();
const bool bIsChildMovable = ExistingActor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass());
if (!bIsPointPlaceable && !bIsChildMovable) return;
ActivePlacementActor = ExistingActor; ActivePlacementActor = ExistingActor;
bAppendingToActor = true; bAppendingToActor = true;
EditorMode = EPS_Editor_EditorMode::SplinePlacement; EditorMode = EPS_Editor_EditorMode::SplinePlacement;
// Store initial state for undo (spline-specific) // Store initial state and show handles
if (IPS_Editor_SplineEditable* Spline = Cast<IPS_Editor_SplineEditable>(ExistingActor)) if (IPS_Editor_SplineEditable* Spline = Cast<IPS_Editor_SplineEditable>(ExistingActor))
{ {
AppendInitialPoints = Spline->GetAllPointLocations(); AppendInitialPoints = Spline->GetAllPointLocations();
Spline->SetHandlesVisible(true); Spline->SetHandlesVisible(true);
} }
else if (bIsChildMovable)
{
AppendInitialPoints = IPS_Editor_ChildMovable::Execute_GetAllChildLocations(ExistingActor);
IPS_Editor_ChildMovable::Execute_SetChildHandlesVisible(ExistingActor, true);
}
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Appending points to %s"), *ExistingActor->GetName()); UE_LOG(LogTemp, Log, TEXT("PS_Editor: Editing sub-elements of %s"), *ExistingActor->GetName());
} }
void APS_Editor_PlayerController::FinishPointPlacement() void APS_Editor_PlayerController::FinishPointPlacement()
@@ -332,6 +345,11 @@ void APS_Editor_PlayerController::FinishPointPlacement()
Spline->SetHandlesVisible(false); Spline->SetHandlesVisible(false);
Spline->ClearHandleHighlight(); Spline->ClearHandleHighlight();
} }
else if (PlacementActor && PlacementActor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass()))
{
IPS_Editor_ChildMovable::Execute_SetChildHandlesVisible(PlacementActor, false);
IPS_Editor_ChildMovable::Execute_ClearChildHighlight(PlacementActor);
}
if (PlacementActor && SelectionManager) if (PlacementActor && SelectionManager)
{ {
@@ -452,6 +470,9 @@ void APS_Editor_PlayerController::DisableSublevelPostProcessMaterials()
ULevel* Level = CurrentSublevel->GetLoadedLevel(); ULevel* Level = CurrentSublevel->GetLoadedLevel();
if (!Level) return; if (!Level) return;
// Force lighting/rendering resources to initialize properly for baked lightmaps
Level->InitializeRenderingResources();
APS_Editor_CameraStart* CameraStart = nullptr; APS_Editor_CameraStart* CameraStart = nullptr;
for (AActor* Actor : Level->Actors) for (AActor* Actor : Level->Actors)

View File

@@ -1,5 +1,6 @@
#include "PS_Editor_UndoManager.h" #include "PS_Editor_UndoManager.h"
#include "PS_Editor_SplineEditable.h" #include "PS_Editor_SplineEditable.h"
#include "PS_Editor_ChildMovable.h"
#include "UObject/UnrealType.h" #include "UObject/UnrealType.h"
#include "Components/ActorComponent.h" #include "Components/ActorComponent.h"
@@ -9,6 +10,10 @@ void FPS_Editor_SplinePointAction::Execute()
{ {
Spline->SetAllPointLocations(NewPoints); Spline->SetAllPointLocations(NewPoints);
} }
else if (SplineActor.IsValid() && SplineActor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass()))
{
IPS_Editor_ChildMovable::Execute_SetAllChildLocations(SplineActor.Get(), NewPoints);
}
} }
void FPS_Editor_SplinePointAction::Undo() void FPS_Editor_SplinePointAction::Undo()
@@ -17,6 +22,10 @@ void FPS_Editor_SplinePointAction::Undo()
{ {
Spline->SetAllPointLocations(OldPoints); Spline->SetAllPointLocations(OldPoints);
} }
else if (SplineActor.IsValid() && SplineActor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass()))
{
IPS_Editor_ChildMovable::Execute_SetAllChildLocations(SplineActor.Get(), OldPoints);
}
} }
void FPS_Editor_PropertyAction::ApplyValue(const FString& ValueStr) void FPS_Editor_PropertyAction::ApplyValue(const FString& ValueStr)

View File

@@ -25,6 +25,10 @@ struct FPS_Editor_ActorData
/** Custom property values: key = "ComponentName.PropertyName", value = text-serialized value. */ /** Custom property values: key = "ComponentName.PropertyName", value = text-serialized value. */
UPROPERTY() UPROPERTY()
TMap<FString, FString> CustomProperties; TMap<FString, FString> CustomProperties;
/** True if this actor is tagged as preload (briefing/placement phase). */
UPROPERTY()
bool bPreload = false;
}; };
/** Serialized data for a complete scene. */ /** Serialized data for a complete scene. */

View File

@@ -2,6 +2,7 @@
#include "PS_Editor_SceneData.h" #include "PS_Editor_SceneData.h"
#include "PS_Editor_EditableComponent.h" #include "PS_Editor_EditableComponent.h"
#include "PS_Editor_EditableInterface.h" #include "PS_Editor_EditableInterface.h"
#include "PS_Editor_ChildMovable.h"
#include "PS_Editor_SpawnableComponent.h" #include "PS_Editor_SpawnableComponent.h"
#include "PS_Editor_SplineActor.h" #include "PS_Editor_SplineActor.h"
#include "PS_Editor_SplineEditable.h" #include "PS_Editor_SplineEditable.h"
@@ -12,7 +13,7 @@
#include "UObject/UnrealType.h" #include "UObject/UnrealType.h"
#include "Components/ActorComponent.h" #include "Components/ActorComponent.h"
bool UPS_Editor_SceneLoader::LoadScene(const UObject* WorldContextObject, const FString& SceneName, TArray<AActor*>& OutActors, bool bStripEditorComponents) bool UPS_Editor_SceneLoader::LoadScene(const UObject* WorldContextObject, const FString& SceneName, TArray<AActor*>& OutActors, bool bStripEditorComponents, EPS_Editor_SceneLoadFilter Filter)
{ {
OutActors.Empty(); OutActors.Empty();
@@ -44,7 +45,7 @@ bool UPS_Editor_SceneLoader::LoadScene(const UObject* WorldContextObject, const
return false; return false;
} }
if (!LoadSceneFromData(World, SceneData, OutActors)) if (!LoadSceneFromData(World, SceneData, OutActors, Filter))
{ {
return false; return false;
} }
@@ -87,7 +88,7 @@ bool UPS_Editor_SceneLoader::LoadScene(const UObject* WorldContextObject, const
return true; return true;
} }
bool UPS_Editor_SceneLoader::LoadSceneFromData(UWorld* World, const FPS_Editor_SceneData& SceneData, TArray<AActor*>& OutActors) bool UPS_Editor_SceneLoader::LoadSceneFromData(UWorld* World, const FPS_Editor_SceneData& SceneData, TArray<AActor*>& OutActors, EPS_Editor_SceneLoadFilter Filter)
{ {
if (!World) if (!World)
{ {
@@ -99,7 +100,7 @@ bool UPS_Editor_SceneLoader::LoadSceneFromData(UWorld* World, const FPS_Editor_S
bIsCurrentlyLoading = true; bIsCurrentlyLoading = true;
for (const FPS_Editor_ActorData& ActorData : SceneData.Actors) for (const FPS_Editor_ActorData& ActorData : SceneData.Actors)
{ {
AActor* SpawnedActor = SpawnActorFromData(World, ActorData); AActor* SpawnedActor = SpawnActorFromData(World, ActorData, Filter);
if (SpawnedActor) if (SpawnedActor)
{ {
OutActors.Add(SpawnedActor); OutActors.Add(SpawnedActor);
@@ -113,8 +114,12 @@ bool UPS_Editor_SceneLoader::LoadSceneFromData(UWorld* World, const FPS_Editor_S
return SpawnedCount > 0; return SpawnedCount > 0;
} }
AActor* UPS_Editor_SceneLoader::SpawnActorFromData(UWorld* World, const FPS_Editor_ActorData& ActorData) AActor* UPS_Editor_SceneLoader::SpawnActorFromData(UWorld* World, const FPS_Editor_ActorData& ActorData, EPS_Editor_SceneLoadFilter Filter)
{ {
// Apply preload filter
if (Filter == EPS_Editor_SceneLoadFilter::PreloadOnly && !ActorData.bPreload) return nullptr;
if (Filter == EPS_Editor_SceneLoadFilter::ExcludePreload && ActorData.bPreload) return nullptr;
// Load class (supports both Blueprint and C++ classes) // Load class (supports both Blueprint and C++ classes)
UClass* LoadedClass = LoadObject<UClass>(nullptr, *ActorData.ClassPath); UClass* LoadedClass = LoadObject<UClass>(nullptr, *ActorData.ClassPath);
if (!LoadedClass) if (!LoadedClass)
@@ -156,6 +161,12 @@ AActor* UPS_Editor_SceneLoader::SpawnActorFromData(UWorld* World, const FPS_Edit
Spline->ImportFromCustomProperties(ActorData.CustomProperties); Spline->ImportFromCustomProperties(ActorData.CustomProperties);
} }
// Import child movable data
if (SpawnedActor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass()))
{
IPS_Editor_ChildMovable::Execute_ImportChildLocations(SpawnedActor, ActorData.CustomProperties);
}
// Notify the actor that all properties are ready // Notify the actor that all properties are ready
if (SpawnedActor->GetClass()->ImplementsInterface(UPS_Editor_EditableInterface::StaticClass())) if (SpawnedActor->GetClass()->ImplementsInterface(UPS_Editor_EditableInterface::StaticClass()))
{ {
@@ -211,6 +222,7 @@ void UPS_Editor_SceneLoader::ApplyCustomProperties(AActor* Actor, const TMap<FSt
} }
bool UPS_Editor_SceneLoader::bIsCurrentlyLoading = false; bool UPS_Editor_SceneLoader::bIsCurrentlyLoading = false;
bool UPS_Editor_SceneLoader::bIsEditorModeActive = false;
bool UPS_Editor_SceneLoader::IsLoadingFromScene(const AActor* Actor) bool UPS_Editor_SceneLoader::IsLoadingFromScene(const AActor* Actor)
{ {

View File

@@ -5,6 +5,15 @@
#include "Kismet/BlueprintFunctionLibrary.h" #include "Kismet/BlueprintFunctionLibrary.h"
#include "PS_Editor_SceneLoader.generated.h" #include "PS_Editor_SceneLoader.generated.h"
/** Filter for selective scene loading. */
UENUM(BlueprintType)
enum class EPS_Editor_SceneLoadFilter : uint8
{
All, /** Load all actors. */
PreloadOnly, /** Load only actors tagged as preload (briefing/placement). */
ExcludePreload /** Load all actors except those tagged as preload. */
};
/** /**
* Standalone scene loader for PS_Editor. * Standalone scene loader for PS_Editor.
* Loads a JSON scene file and spawns all actors with their properties and spline data. * Loads a JSON scene file and spawns all actors with their properties and spline data.
@@ -37,13 +46,15 @@ public:
* @return True if the scene was loaded successfully. * @return True if the scene was loaded successfully.
*/ */
UFUNCTION(BlueprintCallable, Category = "PS Editor", meta = (WorldContext = "WorldContextObject")) UFUNCTION(BlueprintCallable, Category = "PS Editor", meta = (WorldContext = "WorldContextObject"))
static bool LoadScene(const UObject* WorldContextObject, const FString& SceneName, TArray<AActor*>& OutActors, bool bStripEditorComponents = true); static bool LoadScene(const UObject* WorldContextObject, const FString& SceneName, TArray<AActor*>& OutActors,
bool bStripEditorComponents = true, EPS_Editor_SceneLoadFilter Filter = EPS_Editor_SceneLoadFilter::All);
/** /**
* Load scene from an already-parsed SceneData struct. * Load scene from an already-parsed SceneData struct.
* Useful if you want to parse the JSON yourself or modify data before spawning. * Useful if you want to parse the JSON yourself or modify data before spawning.
*/ */
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);
/** /**
* Get the directory where scenes are stored. * Get the directory where scenes are stored.
@@ -80,6 +91,13 @@ public:
/** Static flag: true while SceneLoader or SceneSerializer is spawning actors. */ /** Static flag: true while SceneLoader or SceneSerializer is spawning actors. */
static bool bIsCurrentlyLoading; static bool bIsCurrentlyLoading;
/** Static flag: true while the PS_Editor runtime editor is active. */
static bool bIsEditorModeActive;
/** Returns true if the PS_Editor runtime editor is currently active. Callable from any BP. */
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "PS Editor")
static bool IsInEditorMode() { return bIsEditorModeActive; }
/** /**
* Get all saved scenes for the current level. * Get all saved scenes for the current level.
*/ */
@@ -87,8 +105,8 @@ public:
static TArray<FString> GetSavedSceneNamesForCurrentLevel(const UObject* WorldContextObject); static TArray<FString> GetSavedSceneNamesForCurrentLevel(const UObject* WorldContextObject);
private: private:
/** Spawn a single actor from saved data and apply all properties. */ /** Spawn a single actor from saved data and apply all properties. Returns nullptr if filtered out. */
static AActor* SpawnActorFromData(UWorld* World, const FPS_Editor_ActorData& ActorData); static AActor* SpawnActorFromData(UWorld* World, const FPS_Editor_ActorData& ActorData, EPS_Editor_SceneLoadFilter Filter);
/** 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);

View File

@@ -3,6 +3,8 @@
#include "PS_Editor_SelectionManager.h" #include "PS_Editor_SelectionManager.h"
#include "PS_Editor_EditableComponent.h" #include "PS_Editor_EditableComponent.h"
#include "PS_Editor_EditableInterface.h" #include "PS_Editor_EditableInterface.h"
#include "PS_Editor_SpawnableComponent.h"
#include "PS_Editor_ChildMovable.h"
#include "PS_Editor_PlayerController.h" #include "PS_Editor_PlayerController.h"
#include "PS_Editor_SceneLoader.h" #include "PS_Editor_SceneLoader.h"
#include "PS_Editor_SplineActor.h" #include "PS_Editor_SplineActor.h"
@@ -49,6 +51,12 @@ bool UPS_Editor_SceneSerializer::SaveScene(const FString& SceneName, UPS_Editor_
ActorData.Rotation = Actor->GetActorRotation(); ActorData.Rotation = Actor->GetActorRotation();
ActorData.Scale = Actor->GetActorScale3D(); ActorData.Scale = Actor->GetActorScale3D();
// Save preload tag
if (UPS_Editor_SpawnableComponent* SpawnComp = Actor->FindComponentByClass<UPS_Editor_SpawnableComponent>())
{
ActorData.bPreload = SpawnComp->bPreload;
}
// Export spline data // Export spline data
if (IPS_Editor_SplineEditable* Spline = Cast<IPS_Editor_SplineEditable>(Actor)) if (IPS_Editor_SplineEditable* Spline = Cast<IPS_Editor_SplineEditable>(Actor))
{ {
@@ -57,6 +65,12 @@ bool UPS_Editor_SceneSerializer::SaveScene(const FString& SceneName, UPS_Editor_
Spline->GetNumPoints(), *ActorData.ClassPath); Spline->GetNumPoints(), *ActorData.ClassPath);
} }
// Export child movable data
if (Actor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass()))
{
IPS_Editor_ChildMovable::Execute_ExportChildLocations(Actor, ActorData.CustomProperties);
}
// Export custom editable properties // Export custom editable properties
if (UPS_Editor_EditableComponent* EditComp = Actor->FindComponentByClass<UPS_Editor_EditableComponent>()) if (UPS_Editor_EditableComponent* EditComp = Actor->FindComponentByClass<UPS_Editor_EditableComponent>())
{ {
@@ -171,6 +185,15 @@ bool UPS_Editor_SceneSerializer::LoadScene(const FString& SceneName, UPS_Editor_
AActor* SpawnedActor = SpawnManager->SpawnActorDirect(LoadedClass, ActorData.Location, ActorData.Rotation, ActorData.Scale); AActor* SpawnedActor = SpawnManager->SpawnActorDirect(LoadedClass, ActorData.Location, ActorData.Rotation, ActorData.Scale);
if (SpawnedActor) if (SpawnedActor)
{ {
// Restore preload tag
if (ActorData.bPreload)
{
if (UPS_Editor_SpawnableComponent* SpawnComp = SpawnedActor->FindComponentByClass<UPS_Editor_SpawnableComponent>())
{
SpawnComp->bPreload = true;
}
}
// Import custom properties // Import custom properties
if (ActorData.CustomProperties.Num() > 0) if (ActorData.CustomProperties.Num() > 0)
{ {
@@ -222,6 +245,12 @@ bool UPS_Editor_SceneSerializer::LoadScene(const FString& SceneName, UPS_Editor_
Spline->ImportFromCustomProperties(ActorData.CustomProperties); Spline->ImportFromCustomProperties(ActorData.CustomProperties);
} }
// Import child movable data
if (SpawnedActor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass()))
{
IPS_Editor_ChildMovable::Execute_ImportChildLocations(SpawnedActor, ActorData.CustomProperties);
}
// Notify the actor that properties were loaded and we are in editor mode // Notify the actor that properties were loaded and we are in editor mode
if (SpawnedActor->GetClass()->ImplementsInterface(UPS_Editor_EditableInterface::StaticClass())) if (SpawnedActor->GetClass()->ImplementsInterface(UPS_Editor_EditableInterface::StaticClass()))
{ {

View File

@@ -0,0 +1,71 @@
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "PS_Editor_ChildMovable.generated.h"
UINTERFACE(BlueprintType, meta = (DisplayName = "PS Editor Child Movable"))
class PS_EDITOR_API UPS_Editor_ChildMovable : public UInterface
{
GENERATED_BODY()
};
/**
* Interface for actors containing sub-elements (child actors, components)
* that can be individually selected and moved in the PS_Editor runtime editor.
*
* Implement this in your Blueprint to enable per-child editing.
* The plugin handles selection, gizmo, undo, and serialization.
*
* Example: VR placement root with 10 movable placement spots.
*/
class PS_EDITOR_API IPS_Editor_ChildMovable
{
GENERATED_BODY()
public:
/** Number of movable sub-elements. */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable")
int32 GetMovableChildCount() const;
/** World location of the sub-element at the given index. */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable")
FVector GetChildLocation(int32 Index) const;
/** Move the sub-element at the given index to a new world position. */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable")
void MoveChild(int32 Index, const FVector& NewWorldPosition);
/** Return all sub-element positions (used for undo snapshots). */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable")
TArray<FVector> GetAllChildLocations() const;
/** Restore all sub-element positions (used for undo). */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable")
void SetAllChildLocations(const TArray<FVector>& Positions);
/** Index of the sub-element under the cursor ray (-1 if none).
* Implement with sphere-ray intersection or any hit detection that suits your visuals. */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable")
int32 GetChildIndexUnderCursor(const FVector& RayOrigin, const FVector& RayDirection) const;
/** Show or hide visual handles for the sub-elements. */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable")
void SetChildHandlesVisible(bool bVisible);
/** Highlight a specific sub-element (e.g. change handle color). */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable")
void HighlightChild(int32 Index);
/** Clear all sub-element highlights. */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable")
void ClearChildHighlight();
/** Export sub-element positions to the CustomProperties map for scene saving. */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable")
void ExportChildLocations(TMap<FString, FString>& OutProperties) const;
/** Import sub-element positions from the CustomProperties map on scene load. */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable")
void ImportChildLocations(const TMap<FString, FString>& Properties);
};

View File

@@ -47,4 +47,9 @@ public:
/** If true, scaling is always uniform on all axes. Dragging any single axis scales all three equally. */ /** If true, scaling is always uniform on all axes. Dragging any single axis scales all three equally. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spawnable") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spawnable")
bool bUniformScaleOnly = false; bool bUniformScaleOnly = false;
/** If true, this actor belongs to the preload/briefing phase.
* Use scene loading filters to load only preload actors or exclude them. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spawnable")
bool bPreload = false;
}; };

View File

@@ -10,6 +10,7 @@
#include "PS_Editor_SpawnableComponent.h" #include "PS_Editor_SpawnableComponent.h"
#include "PS_Editor_SplineActor.h" #include "PS_Editor_SplineActor.h"
#include "PS_Editor_SplineEditable.h" #include "PS_Editor_SplineEditable.h"
#include "PS_Editor_ChildMovable.h"
#include "PS_Editor_Pawn.h" #include "PS_Editor_Pawn.h"
#include "PS_Editor_PointPlaceable.h" #include "PS_Editor_PointPlaceable.h"
#include "Widgets/SBoxPanel.h" #include "Widgets/SBoxPanel.h"
@@ -222,6 +223,39 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::RebuildWidget()
.ColorAndOpacity(FLinearColor(1.0f, 0.6f, 0.0f)) .ColorAndOpacity(FLinearColor(1.0f, 0.6f, 0.0f))
] ]
] ]
+ SHorizontalBox::Slot().AutoWidth().Padding(8.0f, 0.0f, 0.0f, 0.0f).VAlign(VAlign_Center)
[
SAssignNew(ChildEditButton, SButton)
.Visibility(EVisibility::Collapsed)
.OnClicked_Lambda([this]() -> FReply
{
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
{
if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement)
{
EditorPC->FinishPointPlacement();
}
else if (UPS_Editor_SelectionManager* SelMgr = SelectionManager.Get())
{
for (const TWeakObjectPtr<AActor>& Weak : SelMgr->GetSelectedActors())
{
if (Weak.IsValid() && Weak->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass()))
{
EditorPC->BeginPointPlacementAppend(Weak.Get());
break;
}
}
}
}
return FReply::Handled();
})
[
SNew(STextBlock)
.Text(FText::FromString(TEXT("Edit Children")))
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 10))
.ColorAndOpacity(FLinearColor(0.3f, 0.8f, 1.0f))
]
]
+ SHorizontalBox::Slot().AutoWidth().Padding(8.0f, 0.0f, 0.0f, 0.0f).VAlign(VAlign_Center) + SHorizontalBox::Slot().AutoWidth().Padding(8.0f, 0.0f, 0.0f, 0.0f).VAlign(VAlign_Center)
[ [
SAssignNew(SplineDeletePointButton, SButton) SAssignNew(SplineDeletePointButton, SButton)
@@ -831,6 +865,7 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
} }
// Notify all spawned actors: leaving editor mode // Notify all spawned actors: leaving editor mode
UPS_Editor_SceneLoader::bIsEditorModeActive = false;
if (UPS_Editor_SpawnManager* PlaySM2 = EditorPC->GetSpawnManager()) if (UPS_Editor_SpawnManager* PlaySM2 = EditorPC->GetSpawnManager())
{ {
PlaySM2->NotifyEditorModeChanged(false); PlaySM2->NotifyEditorModeChanged(false);
@@ -1425,8 +1460,9 @@ void UPS_Editor_MainWidget::RefreshPropertiesFromSelection()
} }
} }
// Determine if a spline is selected (for Edit button) // Determine if a spline or child-movable is selected (for Edit buttons)
bool bSplineSelected = false; bool bSplineSelected = false;
bool bChildMovableSelected = false;
if (CurrentEditorMode == EPS_Editor_EditorMode::Normal && SM->IsAnythingSelected()) if (CurrentEditorMode == EPS_Editor_EditorMode::Normal && SM->IsAnythingSelected())
{ {
for (const TWeakObjectPtr<AActor>& Weak : SM->GetSelectedActors()) for (const TWeakObjectPtr<AActor>& Weak : SM->GetSelectedActors())
@@ -1436,6 +1472,11 @@ void UPS_Editor_MainWidget::RefreshPropertiesFromSelection()
bSplineSelected = true; bSplineSelected = true;
break; break;
} }
if (Weak.IsValid() && Weak->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass()))
{
bChildMovableSelected = true;
break;
}
} }
} }
@@ -1461,9 +1502,22 @@ void UPS_Editor_MainWidget::RefreshPropertiesFromSelection()
HelpBarText->SetColorAndOpacity(FLinearColor(1.0f, 0.6f, 0.0f)); HelpBarText->SetColorAndOpacity(FLinearColor(1.0f, 0.6f, 0.0f));
} }
if (SplineEditButton.IsValid()) SplineEditButton->SetVisibility(EVisibility::Collapsed); if (SplineEditButton.IsValid()) SplineEditButton->SetVisibility(EVisibility::Collapsed);
if (ChildEditButton.IsValid()) ChildEditButton->SetVisibility(EVisibility::Collapsed);
if (SplineDeletePointButton.IsValid()) SplineDeletePointButton->SetVisibility(bPointSelected ? EVisibility::Visible : EVisibility::Collapsed); if (SplineDeletePointButton.IsValid()) SplineDeletePointButton->SetVisibility(bPointSelected ? EVisibility::Visible : EVisibility::Collapsed);
if (SplineFinishButton.IsValid()) SplineFinishButton->SetVisibility(EVisibility::Visible); if (SplineFinishButton.IsValid()) SplineFinishButton->SetVisibility(EVisibility::Visible);
} }
else if (bChildMovableSelected)
{
if (HelpBarText.IsValid())
{
HelpBarText->SetText(FText::FromString(TEXT("Child-movable selected — click Edit Children to move sub-elements")));
HelpBarText->SetColorAndOpacity(FLinearColor(0.3f, 0.8f, 1.0f));
}
if (SplineEditButton.IsValid()) SplineEditButton->SetVisibility(EVisibility::Collapsed);
if (ChildEditButton.IsValid()) ChildEditButton->SetVisibility(EVisibility::Visible);
if (SplineDeletePointButton.IsValid()) SplineDeletePointButton->SetVisibility(EVisibility::Collapsed);
if (SplineFinishButton.IsValid()) SplineFinishButton->SetVisibility(EVisibility::Collapsed);
}
else if (bSplineSelected) else if (bSplineSelected)
{ {
if (HelpBarText.IsValid()) if (HelpBarText.IsValid())
@@ -1483,6 +1537,7 @@ void UPS_Editor_MainWidget::RefreshPropertiesFromSelection()
HelpBarText->SetColorAndOpacity(FLinearColor(0.6f, 0.6f, 0.6f)); HelpBarText->SetColorAndOpacity(FLinearColor(0.6f, 0.6f, 0.6f));
} }
if (SplineEditButton.IsValid()) SplineEditButton->SetVisibility(EVisibility::Collapsed); if (SplineEditButton.IsValid()) SplineEditButton->SetVisibility(EVisibility::Collapsed);
if (ChildEditButton.IsValid()) ChildEditButton->SetVisibility(EVisibility::Collapsed);
if (SplineDeletePointButton.IsValid()) SplineDeletePointButton->SetVisibility(EVisibility::Collapsed); if (SplineDeletePointButton.IsValid()) SplineDeletePointButton->SetVisibility(EVisibility::Collapsed);
if (SplineFinishButton.IsValid()) SplineFinishButton->SetVisibility(EVisibility::Collapsed); if (SplineFinishButton.IsValid()) SplineFinishButton->SetVisibility(EVisibility::Collapsed);
} }

View File

@@ -113,6 +113,7 @@ private:
TSharedPtr<SWidget> SplineFinishButton; TSharedPtr<SWidget> SplineFinishButton;
TSharedPtr<SWidget> SplineEditButton; TSharedPtr<SWidget> SplineEditButton;
TSharedPtr<SWidget> SplineDeletePointButton; TSharedPtr<SWidget> SplineDeletePointButton;
TSharedPtr<SWidget> ChildEditButton;
// Simulate button // Simulate button
TSharedPtr<STextBlock> SimulateButtonText; TSharedPtr<STextBlock> SimulateButtonText;