Compare commits

...

7 Commits

Author SHA1 Message Date
fb95d3b821 Simulate mode: AI start/stop, save lock, position restore
- Simulate button (orange) toggles AI on/off in editor
- StartSimulation: saves all actor transforms, RestartLogic on all BrainComponents
- StopSimulation: StopLogic on all BrainComponents, restores saved transforms
- Save blocked during simulation with help bar message
- AI auto-stopped per-pawn on spawn in editor (deferred next tick for AIController)
- SceneSerializer also stops AI on loaded pawns
- AIModule added to Build.cs dependencies

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 20:29:45 +02:00
16fdcb55da Debounced SplineNetwork rebuild on spline point changes
- ScheduleNetworkRebuild() called after AddPoint, RemovePoint, SetAllPointLocations
- 0.5s debounce timer avoids redundant rebuilds during rapid editing
- AI NPCs now detect and follow splines placed in the editor immediately

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 20:07:22 +02:00
1f11b79222 Auto-call OnRep on server after editor property change + ForceNetUpdate
- After ImportText, automatically call the property's RepNotifyFunc via reflection
- ForceNetUpdate ensures replication to clients (OnRep fires on both server and client)
- No BP changes needed — fully transparent for replicated properties

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 19:50:20 +02:00
6c39b5888f CustomDepth stencil 2 on spline meshes, SplineNetwork rebuild, PP occluded (disabled)
- Enable RenderCustomDepth + stencil value 2 on spline tube mesh
- Restore stencil 2 on deselect (selection sets stencil 1 for outline)
- Auto-generated M_PS_Editor_SplineOccluded PP material (stencil + depth compare)
- PP occluded effect disabled for now (depth comparison unreliable)
- AISpline triggers SplineNetwork::RebuildNetwork on BeginPlay (next tick)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 19:18:41 +02:00
24e50ab919 IPS_Editor_SplineEditable interface, spline visibility, snap-to-ground, Play without BaseLevel
Refactor spline system to use IPS_Editor_SplineEditable interface:
- New interface in Spline/PS_Editor_SplineEditable.h with all editing methods
- APS_Editor_SplineActor and APS_BehaviorEditor_AISpline both implement it
- All ~40 Cast<APS_Editor_SplineActor> replaced with Cast<IPS_Editor_SplineEditable>
- UndoManager SplinePointAction uses TWeakObjectPtr<AActor> + interface cast
- Any actor implementing the interface works with full editor spline editing

Spline visibility:
- SetVisualizationVisible() hides tube/handles/cube in gameplay (data stays for AI)
- SceneLoader calls it when stripping editor components
- SplineMatOccluded material added (unused for now, PP occlusion planned)

Other improvements:
- Snap spline CP to ground on placement (downward raytrace)
- Play button works without BaseLevel (uses current editor map)
- AISpline triggers SplineNetwork::RebuildNetwork on BeginPlay (next tick)
- PS_BehaviorEditor marked EnabledByDefault: false (only active in PROSERVE)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 18:05:53 +02:00
21e39ad3fa Add PS_BehaviorEditor bridge plugin with AI Spline actor
New plugin that bridges PS_Editor and PS_AI_Behavior:
- APS_BehaviorEditor_AISpline inherits from APS_AI_Behavior_SplinePath
  (detected by SplineNetwork, usable by BT tasks)
- Implements IPS_Editor_PointPlaceable (click-to-place flow)
- Full tube visualization + handles (adapted from PS_Editor_SplineActor)
- SpawnableComponent (catalogue category "AI") + EditableComponent
- Editable AI properties: SplineCategory, bBidirectional, SplineWalkSpeed, Priority
- Custom serialization for spline points + AI properties
- No modifications to PS_Editor or PS_AI_Behavior plugins

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 15:00:19 +02:00
c04bc2149b Loading screen overlay + camera fade on sublevel load
- Add loading overlay (black background + "Loading..." text) behind UI panels
- Overlay starts visible by default, hidden in NativeConstruct if no sublevel
- CameraFade to black before sublevel load, with deferred fallback for BeginPlay
- 2s delay after OnLevelShown before starting 2s fade-in from black
- ShowLoadingScreen helper on PlayerController via HUD->MainWidget

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 14:26:43 +02:00
22 changed files with 1461 additions and 111 deletions

View File

@@ -0,0 +1,31 @@
{
"FileVersion": 3,
"Version": 1,
"VersionName": "1.0",
"FriendlyName": "PS Behavior Editor",
"Description": "Bridge plugin: AI Behavior splines editable via PS_Editor runtime editor.",
"Category": "PS Tools",
"CreatedBy": "Asterion",
"EnabledByDefault": false,
"Modules": [
{
"Name": "PS_BehaviorEditor",
"Type": "Runtime",
"LoadingPhase": "Default"
}
],
"Plugins": [
{
"Name": "PS_Editor",
"Enabled": true
},
{
"Name": "PS_AI_Behavior",
"Enabled": true
},
{
"Name": "ProceduralMeshComponent",
"Enabled": true
}
]
}

View File

@@ -0,0 +1,19 @@
using UnrealBuildTool;
public class PS_BehaviorEditor : ModuleRules
{
public PS_BehaviorEditor(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[]
{
"Core",
"CoreUObject",
"Engine",
"ProceduralMeshComponent",
"PS_Editor",
"PS_AI_Behavior"
});
}
}

View File

@@ -0,0 +1,3 @@
#include "PS_BehaviorEditor.h"
IMPLEMENT_MODULE(FPS_BehaviorEditorModule, PS_BehaviorEditor)

View File

@@ -0,0 +1,10 @@
#pragma once
#include "Modules/ModuleManager.h"
class FPS_BehaviorEditorModule : public IModuleInterface
{
public:
virtual void StartupModule() override {}
virtual void ShutdownModule() override {}
};

View File

@@ -0,0 +1,531 @@
#include "PS_BehaviorEditor_AISpline.h"
#include "PS_Editor_SpawnableComponent.h"
#include "PS_Editor_EditableComponent.h"
#include "Components/SplineComponent.h"
#include "ProceduralMeshComponent.h"
#include "Components/StaticMeshComponent.h"
#include "Engine/StaticMesh.h"
#include "Materials/MaterialInstanceDynamic.h"
#include "PS_AI_Behavior_SplineNetwork.h"
APS_BehaviorEditor_AISpline::APS_BehaviorEditor_AISpline()
{
PrimaryActorTick.bCanEverTick = false;
// SplineComp is already created by APS_AI_Behavior_SplinePath as root.
// Clear default editor-placed points so we start empty for runtime placement.
if (SplineComp)
{
SplineComp->ClearSplinePoints(false);
SplineComp->SetClosedLoop(false);
}
// Line mesh
SplineMeshComp = CreateDefaultSubobject<UProceduralMeshComponent>(TEXT("SplineMesh"));
SplineMeshComp->SetupAttachment(RootComponent);
SplineMeshComp->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
SplineMeshComp->SetCollisionResponseToAllChannels(ECR_Ignore);
SplineMeshComp->SetCollisionResponseToChannel(ECC_Camera, ECR_Block);
SplineMeshComp->bUseComplexAsSimpleCollision = true;
SplineMeshComp->SetCastShadow(false);
SplineMeshComp->SetRenderCustomDepth(true);
SplineMeshComp->SetCustomDepthStencilValue(2);
// Handle parent
HandleRoot = CreateDefaultSubobject<USceneComponent>(TEXT("HandleRoot"));
HandleRoot->SetupAttachment(RootComponent);
// Spawnable: makes this actor appear in the editor catalogue
SpawnableComp = CreateDefaultSubobject<UPS_Editor_SpawnableComponent>(TEXT("PS_Editor_Spawnable"));
SpawnableComp->DisplayName = TEXT("AI Spline");
SpawnableComp->Category = TEXT("AI");
// Editable: exposes AI + visual properties in the runtime property panel
EditableComp = CreateDefaultSubobject<UPS_Editor_EditableComponent>(TEXT("PS_Editor_Editable"));
EditableComp->EditableProperties.Add({FName(TEXT("SplineCategory"))});
EditableComp->EditableProperties.Add({FName(TEXT("bBidirectional"))});
EditableComp->EditableProperties.Add({FName(TEXT("SplineWalkSpeed"))});
EditableComp->EditableProperties.Add({FName(TEXT("Priority"))});
EditableComp->EditableProperties.Add({FName(TEXT("SplineColor"))});
EditableComp->EditableProperties.Add({FName(TEXT("SplineSelectedColor"))});
}
void APS_BehaviorEditor_AISpline::BeginPlay()
{
Super::BeginPlay();
// Gizmo material (no depth test) for handles and selected state
UMaterialInterface* GizmoMat = LoadObject<UMaterialInterface>(
nullptr, TEXT("/PS_Editor/M_PS_Editor_Gizmo.M_PS_Editor_Gizmo"));
// Opaque depth-tested material for spline line
UMaterialInterface* SplineLineMat = LoadObject<UMaterialInterface>(
nullptr, TEXT("/PS_Editor/M_PS_Editor_SplineLine.M_PS_Editor_SplineLine"));
if (!SplineLineMat)
{
SplineLineMat = LoadObject<UMaterialInterface>(nullptr, TEXT("/PS_Editor/M_PS_Editor_SplineLine"));
}
if (SplineLineMat)
{
SplineMat = UMaterialInstanceDynamic::Create(SplineLineMat, this);
SplineMat->SetVectorParameterValue(TEXT("Color"), SplineColor);
}
if (GizmoMat)
{
SplineMatSelected = UMaterialInstanceDynamic::Create(GizmoMat, this);
SplineMatSelected->SetVectorParameterValue(TEXT("Color"), SplineSelectedColor);
HandleMat = UMaterialInstanceDynamic::Create(GizmoMat, this);
HandleMat->SetVectorParameterValue(TEXT("Color"), FLinearColor(1.0f, 1.0f, 1.0f));
HandleHighlightMat = UMaterialInstanceDynamic::Create(GizmoMat, this);
HandleHighlightMat->SetVectorParameterValue(TEXT("Color"), FLinearColor(1.0f, 1.0f, 0.0f));
CenterCubeMat = UMaterialInstanceDynamic::Create(GizmoMat, this);
CenterCubeMat->SetVectorParameterValue(TEXT("Color"), SplineColor);
SplineMatOccluded = UMaterialInstanceDynamic::Create(GizmoMat, this);
SplineMatOccluded->SetVectorParameterValue(TEXT("Color"), FLinearColor(0.3f, 0.3f, 0.3f));
}
// Center cube (selection handle)
CenterCube = NewObject<UStaticMeshComponent>(this);
CenterCube->SetupAttachment(GetRootComponent());
CenterCube->RegisterComponent();
UStaticMesh* CubeMesh = LoadObject<UStaticMesh>(nullptr, TEXT("/Engine/BasicShapes/Cube.Cube"));
if (CubeMesh)
{
CenterCube->SetStaticMesh(CubeMesh);
}
CenterCube->SetWorldScale3D(FVector(CenterCubeScale));
CenterCube->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
CenterCube->SetCollisionResponseToAllChannels(ECR_Ignore);
CenterCube->SetCollisionResponseToChannel(ECC_Camera, ECR_Block);
CenterCube->SetCastShadow(false);
if (CenterCubeMat)
{
CenterCube->SetMaterial(0, CenterCubeMat);
}
// Rebuild SplineNetwork next tick (all splines from scene load will be ready)
GetWorld()->GetTimerManager().SetTimerForNextTick([WeakWorld = TWeakObjectPtr<UWorld>(GetWorld())]()
{
if (UWorld* World = WeakWorld.Get())
{
if (UPS_AI_Behavior_SplineNetwork* Network = World->GetSubsystem<UPS_AI_Behavior_SplineNetwork>())
{
Network->RebuildNetwork();
}
}
});
}
// ---- Point manipulation ----
void APS_BehaviorEditor_AISpline::ScheduleNetworkRebuild()
{
if (UWorld* World = GetWorld())
{
World->GetTimerManager().ClearTimer(NetworkRebuildTimer);
World->GetTimerManager().SetTimer(NetworkRebuildTimer, [WeakWorld = TWeakObjectPtr<UWorld>(World)]()
{
if (UWorld* W = WeakWorld.Get())
{
if (UPS_AI_Behavior_SplineNetwork* Network = W->GetSubsystem<UPS_AI_Behavior_SplineNetwork>())
{
Network->RebuildNetwork();
}
}
}, 0.5f, false);
}
}
void APS_BehaviorEditor_AISpline::AddPoint(const FVector& WorldPosition)
{
SplineComp->AddSplinePoint(WorldPosition, ESplineCoordinateSpace::World, true);
PointHandles.Add(CreatePointHandle(WorldPosition));
UpdateVisualization();
ScheduleNetworkRebuild();
}
void APS_BehaviorEditor_AISpline::MovePoint(int32 Index, const FVector& NewWorldPosition)
{
if (Index < 0 || Index >= GetNumPoints()) return;
SplineComp->SetLocationAtSplinePoint(Index, NewWorldPosition, ESplineCoordinateSpace::World, true);
if (PointHandles.IsValidIndex(Index) && PointHandles[Index])
{
PointHandles[Index]->SetWorldLocation(NewWorldPosition);
}
UpdateVisualization();
}
void APS_BehaviorEditor_AISpline::RemovePoint(int32 Index)
{
if (Index < 0 || Index >= GetNumPoints()) return;
SplineComp->RemoveSplinePoint(Index, true);
if (PointHandles.IsValidIndex(Index))
{
if (PointHandles[Index]) PointHandles[Index]->DestroyComponent();
PointHandles.RemoveAt(Index);
}
UpdateVisualization();
ScheduleNetworkRebuild();
}
int32 APS_BehaviorEditor_AISpline::GetNumPoints() const
{
return SplineComp ? SplineComp->GetNumberOfSplinePoints() : 0;
}
FVector APS_BehaviorEditor_AISpline::GetPointLocation(int32 Index) const
{
if (!SplineComp || Index < 0 || Index >= GetNumPoints()) return FVector::ZeroVector;
return SplineComp->GetLocationAtSplinePoint(Index, ESplineCoordinateSpace::World);
}
TArray<FVector> APS_BehaviorEditor_AISpline::GetAllPointLocations() const
{
TArray<FVector> Result;
const int32 Num = GetNumPoints();
Result.Reserve(Num);
for (int32 i = 0; i < Num; i++)
{
Result.Add(GetPointLocation(i));
}
return Result;
}
void APS_BehaviorEditor_AISpline::SetAllPointLocations(const TArray<FVector>& Points)
{
if (!SplineComp) return;
SplineComp->ClearSplinePoints(false);
for (const FVector& WorldPos : Points)
{
SplineComp->AddSplinePoint(WorldPos, ESplineCoordinateSpace::World, false);
}
SplineComp->UpdateSpline();
RebuildHandles();
UpdateVisualization();
ScheduleNetworkRebuild();
}
void APS_BehaviorEditor_AISpline::InsertPoint(int32 Index, const FVector& WorldPosition)
{
if (!SplineComp) return;
Index = FMath::Clamp(Index, 0, GetNumPoints());
TArray<FVector> Points = GetAllPointLocations();
Points.Insert(WorldPosition, Index);
SetAllPointLocations(Points); // Already calls ScheduleNetworkRebuild
}
int32 APS_BehaviorEditor_AISpline::FindInsertIndex(const FVector& WorldLocation) const
{
if (!SplineComp || GetNumPoints() < 2) return GetNumPoints();
const float InputKey = SplineComp->FindInputKeyClosestToWorldLocation(WorldLocation);
return FMath::Clamp(FMath::FloorToInt(InputKey) + 1, 1, GetNumPoints());
}
FVector APS_BehaviorEditor_AISpline::GetCentroid() const
{
const int32 Num = GetNumPoints();
if (Num == 0) return GetActorLocation();
FVector Centroid = FVector::ZeroVector;
for (int32 i = 0; i < Num; i++)
{
Centroid += GetPointLocation(i);
}
return Centroid / Num;
}
// ---- Visualization ----
void APS_BehaviorEditor_AISpline::UpdateVisualization()
{
if (!SplineMeshComp || !SplineComp || GetNumPoints() < 2)
{
if (SplineMeshComp) SplineMeshComp->ClearAllMeshSections();
return;
}
const float SplineLength = SplineComp->GetSplineLength();
const int32 NumSamples = FMath::Max(2, FMath::CeilToInt(SplineLength / SampleInterval));
const int32 Segs = TubeSegments;
TArray<FVector> Vertices;
TArray<int32> Triangles;
TArray<FVector> Normals;
TArray<FVector2D> UVs;
Vertices.Reserve(NumSamples * Segs);
Normals.Reserve(NumSamples * Segs);
UVs.Reserve(NumSamples * Segs);
for (int32 i = 0; i < NumSamples; i++)
{
const float Distance = (static_cast<float>(i) / (NumSamples - 1)) * SplineLength;
const FVector Location = SplineComp->GetLocationAtDistanceAlongSpline(Distance, ESplineCoordinateSpace::Local);
const FVector SplineRight = SplineComp->GetRightVectorAtDistanceAlongSpline(Distance, ESplineCoordinateSpace::Local);
const FVector SplineUp = SplineComp->GetUpVectorAtDistanceAlongSpline(Distance, ESplineCoordinateSpace::Local);
const float U = static_cast<float>(i) / (NumSamples - 1);
for (int32 j = 0; j < Segs; j++)
{
const float Angle = (static_cast<float>(j) / Segs) * 2.0f * PI;
const FVector Normal = FMath::Cos(Angle) * SplineRight + FMath::Sin(Angle) * SplineUp;
Vertices.Add(Location + Normal * TubeRadius);
Normals.Add(Normal);
UVs.Add(FVector2D(U, static_cast<float>(j) / Segs));
}
}
Triangles.Reserve((NumSamples - 1) * Segs * 6);
for (int32 i = 0; i < NumSamples - 1; i++)
{
for (int32 j = 0; j < Segs; j++)
{
const int32 NextJ = (j + 1) % Segs;
const int32 A = i * Segs + j;
const int32 B = i * Segs + NextJ;
const int32 C = (i + 1) * Segs + j;
const int32 D = (i + 1) * Segs + NextJ;
Triangles.Add(A); Triangles.Add(C); Triangles.Add(B);
Triangles.Add(B); Triangles.Add(C); Triangles.Add(D);
}
}
SplineMeshComp->ClearAllMeshSections();
SplineMeshComp->CreateMeshSection(0, Vertices, Triangles, Normals, UVs, TArray<FColor>(), TArray<FProcMeshTangent>(), true);
SplineMeshComp->UpdateBounds();
SplineMeshComp->MarkRenderStateDirty();
const bool bCurrentlySelected = (PointHandles.Num() > 0 && PointHandles[0] && PointHandles[0]->IsVisible());
UMaterialInstanceDynamic* LineMat = (bCurrentlySelected && SplineMatSelected) ? SplineMatSelected : SplineMat;
if (LineMat)
{
SplineMeshComp->SetMaterial(0, LineMat);
}
UpdateCenterCube();
}
void APS_BehaviorEditor_AISpline::UpdateCenterCube()
{
if (!CenterCube) return;
if (GetNumPoints() == 0)
{
CenterCube->SetVisibility(false);
return;
}
FVector Centroid = FVector::ZeroVector;
for (int32 i = 0; i < GetNumPoints(); i++)
{
Centroid += GetPointLocation(i);
}
Centroid /= GetNumPoints();
CenterCube->SetWorldLocation(Centroid);
CenterCube->SetVisibility(true);
}
void APS_BehaviorEditor_AISpline::UpdateSplineMaterials()
{
if (SplineMat) SplineMat->SetVectorParameterValue(TEXT("Color"), SplineColor);
if (SplineMatSelected) SplineMatSelected->SetVectorParameterValue(TEXT("Color"), SplineSelectedColor);
if (CenterCubeMat) CenterCubeMat->SetVectorParameterValue(TEXT("Color"), SplineColor);
}
// ---- Handle interaction ----
UStaticMeshComponent* APS_BehaviorEditor_AISpline::CreatePointHandle(const FVector& WorldPosition)
{
UStaticMeshComponent* Handle = NewObject<UStaticMeshComponent>(this);
Handle->SetupAttachment(HandleRoot);
Handle->RegisterComponent();
UStaticMesh* SphereMesh = LoadObject<UStaticMesh>(nullptr, TEXT("/Engine/BasicShapes/Sphere.Sphere"));
if (SphereMesh) Handle->SetStaticMesh(SphereMesh);
Handle->SetWorldLocation(WorldPosition);
Handle->SetWorldScale3D(FVector(HandleScale));
Handle->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
Handle->SetCollisionResponseToAllChannels(ECR_Ignore);
Handle->SetCollisionResponseToChannel(ECC_Camera, ECR_Block);
Handle->SetCastShadow(false);
Handle->SetRenderInDepthPass(false);
if (HandleMat) Handle->SetMaterial(0, HandleMat);
return Handle;
}
void APS_BehaviorEditor_AISpline::RebuildHandles()
{
for (UStaticMeshComponent* Handle : PointHandles)
{
if (Handle) Handle->DestroyComponent();
}
PointHandles.Empty();
for (int32 i = 0; i < GetNumPoints(); i++)
{
PointHandles.Add(CreatePointHandle(GetPointLocation(i)));
}
}
int32 APS_BehaviorEditor_AISpline::GetHandleIndexUnderCursor(const FVector& RayOrigin, const FVector& RayDirection, float SphereRadius) const
{
int32 BestIndex = -1;
float BestDist = MAX_FLT;
for (int32 i = 0; i < PointHandles.Num(); i++)
{
if (!PointHandles[i] || !PointHandles[i]->IsVisible()) continue;
const FVector HandlePos = PointHandles[i]->GetComponentLocation();
const FVector ToHandle = HandlePos - RayOrigin;
const float Proj = FVector::DotProduct(ToHandle, RayDirection);
if (Proj < 0.0f) continue;
const FVector ClosestPoint = RayOrigin + RayDirection * Proj;
const float Dist = FVector::Dist(ClosestPoint, HandlePos);
if (Dist < SphereRadius && Proj < BestDist)
{
BestDist = Proj;
BestIndex = i;
}
}
return BestIndex;
}
void APS_BehaviorEditor_AISpline::SetHandlesVisible(bool bVisible)
{
for (UStaticMeshComponent* Handle : PointHandles)
{
if (Handle)
{
Handle->SetVisibility(bVisible);
Handle->SetCollisionEnabled(bVisible ? ECollisionEnabled::QueryOnly : ECollisionEnabled::NoCollision);
}
}
}
void APS_BehaviorEditor_AISpline::SetSelected(bool bSelected)
{
if (SplineMeshComp)
{
UMaterialInstanceDynamic* Mat = bSelected ? SplineMatSelected : SplineMat;
if (Mat) SplineMeshComp->SetMaterial(0, Mat);
SplineMeshComp->SetCustomDepthStencilValue(bSelected ? 1 : 2);
}
if (CenterCubeMat)
{
CenterCubeMat->SetVectorParameterValue(TEXT("Color"), bSelected ? SplineSelectedColor : SplineColor);
}
}
bool APS_BehaviorEditor_AISpline::IsCenterCube(const UPrimitiveComponent* Comp) const
{
return Comp && Comp == CenterCube;
}
bool APS_BehaviorEditor_AISpline::IsSplineMesh(const UPrimitiveComponent* Comp) const
{
return Comp && Comp == SplineMeshComp;
}
void APS_BehaviorEditor_AISpline::HighlightHandle(int32 Index)
{
for (int32 i = 0; i < PointHandles.Num(); i++)
{
if (PointHandles[i])
{
PointHandles[i]->SetMaterial(0, (i == Index) ? HandleHighlightMat : HandleMat);
}
}
}
void APS_BehaviorEditor_AISpline::ClearHandleHighlight()
{
for (UStaticMeshComponent* Handle : PointHandles)
{
if (Handle && HandleMat) Handle->SetMaterial(0, HandleMat);
}
}
// ---- Visibility ----
void APS_BehaviorEditor_AISpline::SetVisualizationVisible(bool bVisible)
{
if (SplineMeshComp) SplineMeshComp->SetVisibility(bVisible);
if (CenterCube) CenterCube->SetVisibility(bVisible);
if (HandleRoot) HandleRoot->SetVisibility(bVisible, true);
for (UStaticMeshComponent* Handle : PointHandles)
{
if (Handle)
{
Handle->SetVisibility(bVisible);
Handle->SetCollisionEnabled(bVisible ? ECollisionEnabled::QueryOnly : ECollisionEnabled::NoCollision);
}
}
}
// ---- Serialization ----
void APS_BehaviorEditor_AISpline::ExportToCustomProperties(TMap<FString, FString>& OutProperties) const
{
// Spline points
const int32 Num = GetNumPoints();
OutProperties.Add(TEXT("SplinePointCount"), FString::FromInt(Num));
for (int32 i = 0; i < Num; i++)
{
const FVector Loc = GetPointLocation(i);
OutProperties.Add(
FString::Printf(TEXT("SplinePoint_%d"), i),
FString::Printf(TEXT("X=%.4f Y=%.4f Z=%.4f"), Loc.X, Loc.Y, Loc.Z));
}
// Visual properties
OutProperties.Add(TEXT("SplineColor"), SplineColor.ToString());
OutProperties.Add(TEXT("SplineSelectedColor"), SplineSelectedColor.ToString());
// AI properties
OutProperties.Add(TEXT("AISplineCategory"), FString::FromInt(static_cast<int32>(SplineCategory)));
OutProperties.Add(TEXT("AIBidirectional"), bBidirectional ? TEXT("1") : TEXT("0"));
OutProperties.Add(TEXT("AIWalkSpeed"), FString::SanitizeFloat(SplineWalkSpeed));
OutProperties.Add(TEXT("AIPriority"), FString::FromInt(Priority));
}
void APS_BehaviorEditor_AISpline::ImportFromCustomProperties(const TMap<FString, FString>& Properties)
{
// Spline points
const FString* CountStr = Properties.Find(TEXT("SplinePointCount"));
if (CountStr)
{
const int32 Num = FCString::Atoi(**CountStr);
if (Num >= 2)
{
TArray<FVector> Points;
for (int32 i = 0; i < Num; i++)
{
const FString Key = FString::Printf(TEXT("SplinePoint_%d"), i);
const FString* ValStr = Properties.Find(Key);
if (!ValStr) continue;
FVector Loc;
if (Loc.InitFromString(*ValStr)) Points.Add(Loc);
}
if (Points.Num() >= 2) SetAllPointLocations(Points);
}
}
// Visual properties
if (const FString* ColorStr = Properties.Find(TEXT("SplineColor")))
SplineColor.InitFromString(*ColorStr);
if (const FString* SelColorStr = Properties.Find(TEXT("SplineSelectedColor")))
SplineSelectedColor.InitFromString(*SelColorStr);
// AI properties
if (const FString* CatStr = Properties.Find(TEXT("AISplineCategory")))
SplineCategory = static_cast<EPS_AI_Behavior_NPCType>(FCString::Atoi(**CatStr));
if (const FString* BiStr = Properties.Find(TEXT("AIBidirectional")))
bBidirectional = FCString::Atoi(**BiStr) != 0;
if (const FString* SpeedStr = Properties.Find(TEXT("AIWalkSpeed")))
SplineWalkSpeed = FCString::Atof(**SpeedStr);
if (const FString* PriStr = Properties.Find(TEXT("AIPriority")))
Priority = FCString::Atoi(**PriStr);
UpdateSplineMaterials();
}

View File

@@ -0,0 +1,131 @@
#pragma once
#include "CoreMinimal.h"
#include "PS_AI_Behavior_SplinePath.h"
#include "PS_Editor_PointPlaceable.h"
#include "PS_Editor_SplineEditable.h"
#include "PS_BehaviorEditor_AISpline.generated.h"
class UProceduralMeshComponent;
class UPS_Editor_SpawnableComponent;
class UPS_Editor_EditableComponent;
/**
* AI Spline placeable from the PS_Editor runtime editor.
*
* Inherits from APS_AI_Behavior_SplinePath so it is automatically
* detected by the SplineNetwork subsystem and usable by BT tasks.
*
* Implements IPS_Editor_PointPlaceable for click-to-place flow.
* Includes tube visualization + handles from PS_Editor's spline system.
*/
UCLASS(BlueprintType, Blueprintable, meta = (DisplayName = "AI Spline (Editor)"))
class PS_BEHAVIOREDITOR_API APS_BehaviorEditor_AISpline : public APS_AI_Behavior_SplinePath, public IPS_Editor_PointPlaceable, public IPS_Editor_SplineEditable
{
GENERATED_BODY()
public:
APS_BehaviorEditor_AISpline();
// ---- IPS_Editor_PointPlaceable interface ----
virtual void AddPlacementPoint(const FVector& WorldPosition) override { AddPoint(WorldPosition); }
virtual int32 GetMinimumPoints() const override { return 2; }
virtual int32 GetCurrentPointCount() const override { return GetNumPoints(); }
// ---- Visual properties (editable at runtime) ----
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spline|Visual")
FLinearColor SplineColor = FLinearColor(0.2f, 0.6f, 1.0f); // Blue for AI splines
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spline|Visual")
FLinearColor SplineSelectedColor = FLinearColor(0.4f, 0.8f, 1.0f);
// ---- Point manipulation (IPS_Editor_SplineEditable) ----
virtual void AddPoint(const FVector& WorldPosition) override;
virtual void MovePoint(int32 Index, const FVector& NewWorldPosition) override;
virtual void RemovePoint(int32 Index) override;
virtual int32 GetNumPoints() const override;
virtual FVector GetPointLocation(int32 Index) const override;
virtual TArray<FVector> GetAllPointLocations() const override;
virtual void SetAllPointLocations(const TArray<FVector>& Points) override;
virtual void InsertPoint(int32 Index, const FVector& WorldPosition) override;
virtual int32 FindInsertIndex(const FVector& WorldLocation) const override;
virtual FVector GetCentroid() const override;
// ---- Visualization (IPS_Editor_SplineEditable) ----
virtual void UpdateVisualization() override;
UFUNCTION(BlueprintCallable, Category = "PS Editor|Spline")
virtual void UpdateSplineMaterials() override;
// ---- Handle interaction (IPS_Editor_SplineEditable) ----
virtual int32 GetHandleIndexUnderCursor(const FVector& RayOrigin, const FVector& RayDirection, float SphereRadius = 15.0f) const override;
virtual void SetHandlesVisible(bool bVisible) override;
virtual void SetSelected(bool bSelected) override;
virtual bool IsCenterCube(const UPrimitiveComponent* Comp) const override;
virtual bool IsSplineMesh(const UPrimitiveComponent* Comp) const override;
virtual void HighlightHandle(int32 Index) override;
virtual void ClearHandleHighlight() override;
// ---- Visibility (IPS_Editor_SplineEditable) ----
virtual void SetVisualizationVisible(bool bVisible) override;
// ---- Serialization (IPS_Editor_SplineEditable) ----
virtual void ExportToCustomProperties(TMap<FString, FString>& OutProperties) const override;
virtual void ImportFromCustomProperties(const TMap<FString, FString>& Properties) override;
// ---- Access (IPS_Editor_SplineEditable) ----
virtual USplineComponent* GetSplineComponent() const override { return SplineComp; }
protected:
virtual void BeginPlay() override;
private:
UPROPERTY(VisibleAnywhere)
TObjectPtr<UProceduralMeshComponent> SplineMeshComp;
UPROPERTY(VisibleAnywhere)
TObjectPtr<USceneComponent> HandleRoot;
UPROPERTY()
TArray<TObjectPtr<UStaticMeshComponent>> PointHandles;
UPROPERTY()
TObjectPtr<UMaterialInstanceDynamic> SplineMat;
UPROPERTY()
TObjectPtr<UMaterialInstanceDynamic> SplineMatSelected;
UPROPERTY()
TObjectPtr<UMaterialInstanceDynamic> HandleMat;
UPROPERTY()
TObjectPtr<UMaterialInstanceDynamic> HandleHighlightMat;
UPROPERTY()
TObjectPtr<UMaterialInstanceDynamic> SplineMatOccluded;
UPROPERTY()
TObjectPtr<UStaticMeshComponent> CenterCube;
UPROPERTY()
TObjectPtr<UMaterialInstanceDynamic> CenterCubeMat;
/** Schedule a debounced SplineNetwork rebuild. */
void ScheduleNetworkRebuild();
FTimerHandle NetworkRebuildTimer;
UPROPERTY(VisibleAnywhere)
TObjectPtr<UPS_Editor_SpawnableComponent> SpawnableComp;
UPROPERTY(VisibleAnywhere)
TObjectPtr<UPS_Editor_EditableComponent> EditableComp;
UStaticMeshComponent* CreatePointHandle(const FVector& WorldPosition);
void RebuildHandles();
void UpdateCenterCube();
float TubeRadius = 2.0f;
int32 TubeSegments = 8;
float SampleInterval = 10.0f;
float HandleScale = 0.12f;
float CenterCubeScale = 0.10f;
};

View File

@@ -14,6 +14,7 @@
#include "PS_Editor_Gizmo.h" #include "PS_Editor_Gizmo.h"
#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 "Components/SplineComponent.h" #include "Components/SplineComponent.h"
#include "DrawDebugHelpers.h" #include "DrawDebugHelpers.h"
#include "PS_Editor_ConfirmDialog.h" #include "PS_Editor_ConfirmDialog.h"
@@ -52,6 +53,9 @@ void APS_Editor_Pawn::BeginPlay()
OutlinePostProcess->Settings.WeightedBlendables.Array.Add(FWeightedBlendable(1.0f, OutlineMat)); OutlinePostProcess->Settings.WeightedBlendables.Array.Add(FWeightedBlendable(1.0f, OutlineMat));
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Selection outline material loaded")); UE_LOG(LogTemp, Log, TEXT("PS_Editor: Selection outline material loaded"));
} }
// TODO: Spline occluded PP material — disabled for now, depth comparison unreliable
// Consider translucent second-pass approach instead of post-process
else else
{ {
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Outline material /PS_Editor/M_PS_Editor_SelectionOutline not found. " UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Outline material /PS_Editor/M_PS_Editor_SelectionOutline not found. "
@@ -379,7 +383,7 @@ void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value)
// Spline point mode: move the single point instead of actors // Spline point mode: move the single point instead of actors
if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid()) if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid())
{ {
if (APS_Editor_SplineActor* SplineActor = Cast<APS_Editor_SplineActor>(DraggedSplineActor.Get())) if (IPS_Editor_SplineEditable* SplineActor = Cast<IPS_Editor_SplineEditable>(DraggedSplineActor.Get()))
{ {
if (SplinePointDragInitialState.IsValidIndex(ActiveSplinePointIndex)) if (SplinePointDragInitialState.IsValidIndex(ActiveSplinePointIndex))
{ {
@@ -472,7 +476,7 @@ 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
if (APS_Editor_SplineActor* SplineActor = Cast<APS_Editor_SplineActor>(DraggedSplineActor.Get())) if (IPS_Editor_SplineEditable* SplineActor = Cast<IPS_Editor_SplineEditable>(DraggedSplineActor.Get()))
{ {
Gizmo->SetActorLocation(SplineActor->GetPointLocation(ActiveSplinePointIndex)); Gizmo->SetActorLocation(SplineActor->GetPointLocation(ActiveSplinePointIndex));
} }
@@ -480,7 +484,7 @@ void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value)
else if (Selected.Num() == 1) else if (Selected.Num() == 1)
{ {
// Single actor: use centroid for splines, location for others // Single actor: use centroid for splines, location for others
if (APS_Editor_SplineActor* SplineActor = Cast<APS_Editor_SplineActor>(Selected[0].Get())) if (IPS_Editor_SplineEditable* SplineActor = Cast<IPS_Editor_SplineEditable>(Selected[0].Get()))
{ {
Gizmo->SetActorLocation(SplineActor->GetCentroid()); Gizmo->SetActorLocation(SplineActor->GetCentroid());
} }
@@ -507,7 +511,7 @@ void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value)
else if (CameraMode == EPS_Editor_CameraMode::SplinePointDrag) else if (CameraMode == EPS_Editor_CameraMode::SplinePointDrag)
{ {
// Drag spline point along horizontal plane // Drag spline point along horizontal plane
APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(DraggedSplineActor.Get()); IPS_Editor_SplineEditable* Spline = Cast<IPS_Editor_SplineEditable>(DraggedSplineActor.Get());
if (!Spline || ActiveSplinePointIndex < 0) return; if (!Spline || ActiveSplinePointIndex < 0) return;
FVector RayOrigin, RayDir; FVector RayOrigin, RayDir;
@@ -585,23 +589,23 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
if (Mode == EPS_Editor_EditorMode::SplinePlacement || Mode == EPS_Editor_EditorMode::SplinePlacement) if (Mode == EPS_Editor_EditorMode::SplinePlacement || Mode == EPS_Editor_EditorMode::SplinePlacement)
{ {
// Collect candidate splines: selected actors + active placement actor // Collect candidate splines: selected actors + active placement actor
TArray<APS_Editor_SplineActor*> CandidateSplines; TArray<IPS_Editor_SplineEditable*> CandidateSplines;
if (UPS_Editor_SelectionManager* SM = EdPC->GetSelectionManager()) if (UPS_Editor_SelectionManager* SM = EdPC->GetSelectionManager())
{ {
for (const TWeakObjectPtr<AActor>& Weak : SM->GetSelectedActors()) for (const TWeakObjectPtr<AActor>& Weak : SM->GetSelectedActors())
{ {
if (APS_Editor_SplineActor* S = Cast<APS_Editor_SplineActor>(Weak.Get())) if (IPS_Editor_SplineEditable* S = Cast<IPS_Editor_SplineEditable>(Weak.Get()))
{ {
CandidateSplines.AddUnique(S); CandidateSplines.AddUnique(S);
} }
} }
} }
if (APS_Editor_SplineActor* ActiveSpline = Cast<APS_Editor_SplineActor>(EdPC->GetActivePlacementActor())) if (IPS_Editor_SplineEditable* ActiveSpline = Cast<IPS_Editor_SplineEditable>(EdPC->GetActivePlacementActor()))
{ {
CandidateSplines.AddUnique(ActiveSpline); CandidateSplines.AddUnique(ActiveSpline);
} }
for (APS_Editor_SplineActor* Spline : CandidateSplines) for (IPS_Editor_SplineEditable* Spline : CandidateSplines)
{ {
// Center cube check (skip in SplinePlacement — no center cube interaction during creation) // Center cube check (skip in SplinePlacement — no center cube interaction during creation)
if (Mode == EPS_Editor_EditorMode::SplinePlacement) if (Mode == EPS_Editor_EditorMode::SplinePlacement)
@@ -626,7 +630,7 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
if (HandleIdx >= 0) if (HandleIdx >= 0)
{ {
ActiveSplinePointIndex = HandleIdx; ActiveSplinePointIndex = HandleIdx;
DraggedSplineActor = Spline; DraggedSplineActor = Cast<AActor>(Spline);
Spline->HighlightHandle(HandleIdx); Spline->HighlightHandle(HandleIdx);
if (UPS_Editor_SelectionManager* SM = EdPC->GetSelectionManager()) if (UPS_Editor_SelectionManager* SM = EdPC->GetSelectionManager())
@@ -650,16 +654,16 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
if (EdPC2->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement) if (EdPC2->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement)
{ {
// Collect candidate splines: selected + active placement actor // Collect candidate splines: selected + active placement actor
TArray<APS_Editor_SplineActor*> TubeCandidates; TArray<IPS_Editor_SplineEditable*> TubeCandidates;
if (UPS_Editor_SelectionManager* SM2 = EdPC2->GetSelectionManager()) if (UPS_Editor_SelectionManager* SM2 = EdPC2->GetSelectionManager())
{ {
for (const TWeakObjectPtr<AActor>& Weak : SM2->GetSelectedActors()) for (const TWeakObjectPtr<AActor>& Weak : SM2->GetSelectedActors())
{ {
if (APS_Editor_SplineActor* S = Cast<APS_Editor_SplineActor>(Weak.Get())) if (IPS_Editor_SplineEditable* S = Cast<IPS_Editor_SplineEditable>(Weak.Get()))
TubeCandidates.AddUnique(S); TubeCandidates.AddUnique(S);
} }
} }
if (APS_Editor_SplineActor* ActiveSpline = Cast<APS_Editor_SplineActor>(EdPC2->GetActivePlacementActor())) if (IPS_Editor_SplineEditable* ActiveSpline = Cast<IPS_Editor_SplineEditable>(EdPC2->GetActivePlacementActor()))
{ {
TubeCandidates.AddUnique(ActiveSpline); TubeCandidates.AddUnique(ActiveSpline);
} }
@@ -673,7 +677,7 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
if (GetWorld()->LineTraceSingleByChannel(LineHit, RayOrigin, TraceEnd, ECC_Camera, LineParams)) if (GetWorld()->LineTraceSingleByChannel(LineHit, RayOrigin, TraceEnd, ECC_Camera, LineParams))
{ {
for (APS_Editor_SplineActor* Spline : TubeCandidates) for (IPS_Editor_SplineEditable* Spline : TubeCandidates)
{ {
if (Spline->IsSplineMesh(LineHit.GetComponent())) if (Spline->IsSplineMesh(LineHit.GetComponent()))
@@ -687,7 +691,7 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
if (UPS_Editor_UndoManager* UM = EdPC2->GetUndoManager()) if (UPS_Editor_UndoManager* UM = EdPC2->GetUndoManager())
{ {
TSharedPtr<FPS_Editor_SplinePointAction> Action = MakeShared<FPS_Editor_SplinePointAction>(); TSharedPtr<FPS_Editor_SplinePointAction> Action = MakeShared<FPS_Editor_SplinePointAction>();
Action->SplineActor = Spline; Action->SplineActor = Cast<AActor>(Spline);
Action->OldPoints = OldPoints; Action->OldPoints = OldPoints;
Action->NewPoints = Spline->GetAllPointLocations(); Action->NewPoints = Spline->GetAllPointLocations();
Action->Description = TEXT("Insert spline point"); Action->Description = TEXT("Insert spline point");
@@ -695,7 +699,7 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
} }
ActiveSplinePointIndex = InsertIdx; ActiveSplinePointIndex = InsertIdx;
DraggedSplineActor = Spline; DraggedSplineActor = Cast<AActor>(Spline);
Spline->HighlightHandle(InsertIdx); Spline->HighlightHandle(InsertIdx);
if (UPS_Editor_SelectionManager* InsertSM = EdPC2->GetSelectionManager()) if (UPS_Editor_SelectionManager* InsertSM = EdPC2->GetSelectionManager())
@@ -721,13 +725,13 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
{ {
if (EdPC3->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement) if (EdPC3->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement)
{ {
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(EdPC3->GetActivePlacementActor())) if (IPS_Editor_SplineEditable* Spline = Cast<IPS_Editor_SplineEditable>(EdPC3->GetActivePlacementActor()))
{ {
// Raycast to get world click position // Raycast to get world click position
FHitResult AddHit; FHitResult AddHit;
FCollisionQueryParams AddParams; FCollisionQueryParams AddParams;
AddParams.AddIgnoredActor(this); AddParams.AddIgnoredActor(this);
AddParams.AddIgnoredActor(Spline); AddParams.AddIgnoredActor(Cast<AActor>(Spline));
const FVector AddEnd = RayOrigin + RayDir * 100000.0f; const FVector AddEnd = RayOrigin + RayDir * 100000.0f;
FVector ClickPos; FVector ClickPos;
@@ -746,7 +750,7 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
if (UPS_Editor_UndoManager* UM = EdPC3->GetUndoManager()) if (UPS_Editor_UndoManager* UM = EdPC3->GetUndoManager())
{ {
TSharedPtr<FPS_Editor_SplinePointAction> Action = MakeShared<FPS_Editor_SplinePointAction>(); TSharedPtr<FPS_Editor_SplinePointAction> Action = MakeShared<FPS_Editor_SplinePointAction>();
Action->SplineActor = Spline; Action->SplineActor = Cast<AActor>(Spline);
Action->OldPoints = OldPoints; Action->OldPoints = OldPoints;
Action->NewPoints = Spline->GetAllPointLocations(); Action->NewPoints = Spline->GetAllPointLocations();
Action->Description = TEXT("Add spline point"); Action->Description = TEXT("Add spline point");
@@ -756,7 +760,7 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
// Select the new point // Select the new point
const int32 NewIdx = Spline->GetNumPoints() - 1; const int32 NewIdx = Spline->GetNumPoints() - 1;
ActiveSplinePointIndex = NewIdx; ActiveSplinePointIndex = NewIdx;
DraggedSplineActor = Spline; DraggedSplineActor = Cast<AActor>(Spline);
Spline->HighlightHandle(NewIdx); Spline->HighlightHandle(NewIdx);
if (UPS_Editor_SelectionManager* SM3 = EdPC3->GetSelectionManager()) if (UPS_Editor_SelectionManager* SM3 = EdPC3->GetSelectionManager())
@@ -800,10 +804,10 @@ 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())
{ {
if (APS_Editor_SplineActor* SplineActor = Cast<APS_Editor_SplineActor>(DraggedSplineActor.Get())) if (IPS_Editor_SplineEditable* SplineActor = Cast<IPS_Editor_SplineEditable>(DraggedSplineActor.Get()))
{ {
TSharedPtr<FPS_Editor_SplinePointAction> Action = MakeShared<FPS_Editor_SplinePointAction>(); TSharedPtr<FPS_Editor_SplinePointAction> Action = MakeShared<FPS_Editor_SplinePointAction>();
Action->SplineActor = SplineActor; Action->SplineActor = Cast<AActor>(SplineActor);
Action->OldPoints = SplinePointDragInitialState; Action->OldPoints = SplinePointDragInitialState;
Action->NewPoints = SplineActor->GetAllPointLocations(); Action->NewPoints = SplineActor->GetAllPointLocations();
Action->Description = TEXT("Move spline point"); Action->Description = TEXT("Move spline point");
@@ -852,7 +856,7 @@ void APS_Editor_Pawn::HandleLeftClickCompleted(const FInputActionValue& Value)
if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid()) if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid())
{ {
// Keep gizmo at the active spline point // Keep gizmo at the active spline point
if (APS_Editor_SplineActor* SplineActor = Cast<APS_Editor_SplineActor>(DraggedSplineActor.Get())) if (IPS_Editor_SplineEditable* SplineActor = Cast<IPS_Editor_SplineEditable>(DraggedSplineActor.Get()))
{ {
if (APS_Editor_Gizmo* G = SM2->GetGizmo()) if (APS_Editor_Gizmo* G = SM2->GetGizmo())
{ {
@@ -872,14 +876,14 @@ void APS_Editor_Pawn::HandleLeftClickCompleted(const FInputActionValue& Value)
else if (CameraMode == EPS_Editor_CameraMode::SplinePointDrag) else if (CameraMode == EPS_Editor_CameraMode::SplinePointDrag)
{ {
// Record undo for spline point move // Record undo for spline point move
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(DraggedSplineActor.Get())) if (IPS_Editor_SplineEditable* Spline = Cast<IPS_Editor_SplineEditable>(DraggedSplineActor.Get()))
{ {
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController())) if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController()))
{ {
if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager()) if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager())
{ {
TSharedPtr<FPS_Editor_SplinePointAction> Action = MakeShared<FPS_Editor_SplinePointAction>(); TSharedPtr<FPS_Editor_SplinePointAction> Action = MakeShared<FPS_Editor_SplinePointAction>();
Action->SplineActor = Spline; Action->SplineActor = Cast<AActor>(Spline);
Action->OldPoints = SplinePointDragInitialState; Action->OldPoints = SplinePointDragInitialState;
Action->NewPoints = Spline->GetAllPointLocations(); Action->NewPoints = Spline->GetAllPointLocations();
Action->Description = TEXT("Move spline point"); Action->Description = TEXT("Move spline point");
@@ -973,7 +977,7 @@ void APS_Editor_Pawn::HandleDelete(const FInputActionValue& Value)
if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement
&& ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid()) && ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid())
{ {
APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(DraggedSplineActor.Get()); IPS_Editor_SplineEditable* Spline = Cast<IPS_Editor_SplineEditable>(DraggedSplineActor.Get());
if (Spline && Spline->GetNumPoints() > 2) if (Spline && Spline->GetNumPoints() > 2)
{ {
TArray<FVector> OldPoints = Spline->GetAllPointLocations(); TArray<FVector> OldPoints = Spline->GetAllPointLocations();
@@ -982,7 +986,7 @@ void APS_Editor_Pawn::HandleDelete(const FInputActionValue& Value)
if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager()) if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager())
{ {
TSharedPtr<FPS_Editor_SplinePointAction> Action = MakeShared<FPS_Editor_SplinePointAction>(); TSharedPtr<FPS_Editor_SplinePointAction> Action = MakeShared<FPS_Editor_SplinePointAction>();
Action->SplineActor = Spline; Action->SplineActor = Cast<AActor>(Spline);
Action->OldPoints = OldPoints; Action->OldPoints = OldPoints;
Action->NewPoints = Spline->GetAllPointLocations(); Action->NewPoints = Spline->GetAllPointLocations();
Action->Description = TEXT("Delete spline point"); Action->Description = TEXT("Delete spline point");
@@ -1089,7 +1093,7 @@ void APS_Editor_Pawn::HandleSnapToGround(const FInputActionValue& Value)
// If a spline point is selected (in edit or placement mode), snap that point to ground // If a spline point is selected (in edit or placement mode), snap that point to ground
if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid()) if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid())
{ {
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(DraggedSplineActor.Get())) if (IPS_Editor_SplineEditable* Spline = Cast<IPS_Editor_SplineEditable>(DraggedSplineActor.Get()))
{ {
const FVector PointLoc = Spline->GetPointLocation(ActiveSplinePointIndex); const FVector PointLoc = Spline->GetPointLocation(ActiveSplinePointIndex);
const FVector TraceStart = PointLoc + FVector(0.0f, 0.0f, 1.0f); // Just above current position const FVector TraceStart = PointLoc + FVector(0.0f, 0.0f, 1.0f); // Just above current position
@@ -1097,7 +1101,7 @@ void APS_Editor_Pawn::HandleSnapToGround(const FInputActionValue& Value)
FHitResult Hit; FHitResult Hit;
FCollisionQueryParams Params; FCollisionQueryParams Params;
Params.AddIgnoredActor(Spline); Params.AddIgnoredActor(Cast<AActor>(Spline));
Params.AddIgnoredActor(this); Params.AddIgnoredActor(this);
if (UPS_Editor_SelectionManager* SnapSM = EditorPC->GetSelectionManager()) if (UPS_Editor_SelectionManager* SnapSM = EditorPC->GetSelectionManager())
{ {
@@ -1132,7 +1136,7 @@ void APS_Editor_Pawn::HandleSnapToGround(const FInputActionValue& Value)
if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager()) if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager())
{ {
TSharedPtr<FPS_Editor_SplinePointAction> Action = MakeShared<FPS_Editor_SplinePointAction>(); TSharedPtr<FPS_Editor_SplinePointAction> Action = MakeShared<FPS_Editor_SplinePointAction>();
Action->SplineActor = Spline; Action->SplineActor = Cast<AActor>(Spline);
Action->OldPoints = OldPoints; Action->OldPoints = OldPoints;
Action->NewPoints = Spline->GetAllPointLocations(); Action->NewPoints = Spline->GetAllPointLocations();
Action->Description = TEXT("Snap spline point to ground"); Action->Description = TEXT("Snap spline point to ground");
@@ -1438,7 +1442,7 @@ void APS_Editor_Pawn::BeginGizmoDrag(EPS_Editor_GizmoAxis Axis)
// Store initial spline state if editing a point // Store initial spline state if editing a point
if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid()) if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid())
{ {
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(DraggedSplineActor.Get())) if (IPS_Editor_SplineEditable* Spline = Cast<IPS_Editor_SplineEditable>(DraggedSplineActor.Get()))
{ {
SplinePointDragInitialState = Spline->GetAllPointLocations(); SplinePointDragInitialState = Spline->GetAllPointLocations();
} }

View File

@@ -7,12 +7,18 @@
#include "PS_Editor_Gizmo.h" #include "PS_Editor_Gizmo.h"
#include "PS_Editor_Pawn.h" #include "PS_Editor_Pawn.h"
#include "PS_Editor_SplineActor.h" #include "PS_Editor_SplineActor.h"
#include "PS_Editor_SplineEditable.h"
#include "PS_Editor_PointPlaceable.h" #include "PS_Editor_PointPlaceable.h"
#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_HUD.h"
#include "PS_Editor_MainWidget.h"
#include "Camera/PlayerCameraManager.h"
#include "PS_Editor_GameInstanceSubsystem.h" #include "PS_Editor_GameInstanceSubsystem.h"
#include "GameFramework/GameModeBase.h" #include "GameFramework/GameModeBase.h"
#include "AIController.h"
#include "BrainComponent.h"
#include "GameFramework/GameStateBase.h" #include "GameFramework/GameStateBase.h"
#include "GameFramework/PlayerState.h" #include "GameFramework/PlayerState.h"
@@ -28,6 +34,9 @@ void APS_Editor_PlayerController::BeginPlay()
SetInputMode(FInputModeGameAndUI().SetHideCursorDuringCapture(false)); SetInputMode(FInputModeGameAndUI().SetHideCursorDuringCapture(false));
// AI logic will be stopped per-pawn when spawned (see SpawnManager)
// Global StopLogic not available in UE 5.5 — handled per BrainComponent instead
// Create selection manager // Create selection manager
SelectionManager = NewObject<UPS_Editor_SelectionManager>(this); SelectionManager = NewObject<UPS_Editor_SelectionManager>(this);
SelectionManager->Initialize(this); SelectionManager->Initialize(this);
@@ -136,6 +145,14 @@ void APS_Editor_PlayerController::Tick(float DeltaTime)
{ {
Super::Tick(DeltaTime); Super::Tick(DeltaTime);
// Deferred fade-to-black: CameraManager wasn't ready at BeginPlay
if (bPendingFadeToBlack && PlayerCameraManager)
{
PlayerCameraManager->StartCameraFade(0.0f, 1.0f, 0.0f, FLinearColor::Black, true, true);
bPendingFadeToBlack = false;
ShowLoadingScreen(true);
}
if (SpawnManager) if (SpawnManager)
{ {
SpawnManager->TickEditorMode(DeltaTime); SpawnManager->TickEditorMode(DeltaTime);
@@ -153,9 +170,9 @@ void APS_Editor_PlayerController::OnPossess(APawn* InPawn)
} }
} }
APS_Editor_SplineActor* APS_Editor_PlayerController::GetActiveSplineActor() const IPS_Editor_SplineEditable* APS_Editor_PlayerController::GetActiveSplineActor() const
{ {
return Cast<APS_Editor_SplineActor>(ActivePlacementActor); return Cast<IPS_Editor_SplineEditable>(ActivePlacementActor);
} }
void APS_Editor_PlayerController::CloseEditor() void APS_Editor_PlayerController::CloseEditor()
@@ -189,6 +206,87 @@ void APS_Editor_PlayerController::CloseEditor()
OnEditorClosed.Broadcast(); OnEditorClosed.Broadcast();
} }
void APS_Editor_PlayerController::StartSimulation()
{
if (bSimulating) return;
bSimulating = true;
// Save all spawned actor transforms
SimulationSavedTransforms.Empty();
if (SpawnManager)
{
for (const TWeakObjectPtr<AActor>& Weak : SpawnManager->GetSpawnedActors())
{
if (AActor* Actor = Weak.Get())
{
SimulationSavedTransforms.Add(Actor, Actor->GetActorTransform());
}
}
}
// Clear selection
if (SelectionManager)
{
SelectionManager->ClearSelection();
}
// Enable AI logic on all spawned pawns
if (SpawnManager)
{
for (const TWeakObjectPtr<AActor>& Weak : SpawnManager->GetSpawnedActors())
{
if (APawn* P = Cast<APawn>(Weak.Get()))
{
if (AAIController* AIC = Cast<AAIController>(P->GetController()))
{
if (UBrainComponent* Brain = AIC->GetBrainComponent())
{
Brain->RestartLogic();
}
}
}
}
}
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Simulation started (%d actors saved)"), SimulationSavedTransforms.Num());
}
void APS_Editor_PlayerController::StopSimulation()
{
if (!bSimulating) return;
bSimulating = false;
// Disable AI logic on all spawned pawns
if (SpawnManager)
{
for (const TWeakObjectPtr<AActor>& Weak : SpawnManager->GetSpawnedActors())
{
if (APawn* P = Cast<APawn>(Weak.Get()))
{
if (AAIController* AIC = Cast<AAIController>(P->GetController()))
{
if (UBrainComponent* Brain = AIC->GetBrainComponent())
{
Brain->StopLogic(TEXT("PS_Editor"));
}
}
}
}
}
// Restore all saved transforms
for (auto& Pair : SimulationSavedTransforms)
{
if (AActor* Actor = Pair.Key.Get())
{
Actor->SetActorTransform(Pair.Value);
}
}
SimulationSavedTransforms.Empty();
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Simulation stopped, transforms restored"));
}
void APS_Editor_PlayerController::BeginPointPlacement(TSubclassOf<AActor> InActorClass) void APS_Editor_PlayerController::BeginPointPlacement(TSubclassOf<AActor> InActorClass)
{ {
if (SelectionManager) if (SelectionManager)
@@ -212,7 +310,7 @@ void APS_Editor_PlayerController::BeginPointPlacementAppend(AActor* ExistingActo
EditorMode = EPS_Editor_EditorMode::SplinePlacement; EditorMode = EPS_Editor_EditorMode::SplinePlacement;
// Store initial state for undo (spline-specific) // Store initial state for undo (spline-specific)
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(ExistingActor)) if (IPS_Editor_SplineEditable* Spline = Cast<IPS_Editor_SplineEditable>(ExistingActor))
{ {
AppendInitialPoints = Spline->GetAllPointLocations(); AppendInitialPoints = Spline->GetAllPointLocations();
Spline->SetHandlesVisible(true); Spline->SetHandlesVisible(true);
@@ -229,7 +327,7 @@ void APS_Editor_PlayerController::FinishPointPlacement()
AppendInitialPoints.Empty(); AppendInitialPoints.Empty();
EditorMode = EPS_Editor_EditorMode::Normal; EditorMode = EPS_Editor_EditorMode::Normal;
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(PlacementActor)) if (IPS_Editor_SplineEditable* Spline = Cast<IPS_Editor_SplineEditable>(PlacementActor))
{ {
Spline->SetHandlesVisible(false); Spline->SetHandlesVisible(false);
Spline->ClearHandleHighlight(); Spline->ClearHandleHighlight();
@@ -253,6 +351,17 @@ void APS_Editor_PlayerController::CancelPointPlacement()
FinishPointPlacement(); FinishPointPlacement();
} }
void APS_Editor_PlayerController::ShowLoadingScreen(bool bShow)
{
if (APS_Editor_HUD* EditorHUD = Cast<APS_Editor_HUD>(GetHUD()))
{
if (UPS_Editor_MainWidget* Widget = EditorHUD->GetMainWidget())
{
Widget->ShowLoadingScreen(bShow);
}
}
}
void APS_Editor_PlayerController::LoadBaseLevelAsSublevel(const FString& LevelName, const FString& MapPath) void APS_Editor_PlayerController::LoadBaseLevelAsSublevel(const FString& LevelName, const FString& MapPath)
{ {
// Unload previous sublevel first // Unload previous sublevel first
@@ -294,6 +403,19 @@ void APS_Editor_PlayerController::LoadBaseLevelAsSublevel(const FString& LevelNa
return; return;
} }
// Fade to black + show loading overlay
bWaitingForSublevelFadeIn = true;
if (PlayerCameraManager)
{
PlayerCameraManager->StartCameraFade(0.0f, 1.0f, 0.0f, FLinearColor::Black, true, true);
bPendingFadeToBlack = false;
}
else
{
bPendingFadeToBlack = true;
}
ShowLoadingScreen(true);
// Use MapPath if provided, otherwise fall back to LevelName (legacy behavior) // Use MapPath if provided, otherwise fall back to LevelName (legacy behavior)
const FString& LevelToLoad = MapPath.IsEmpty() ? LevelName : MapPath; const FString& LevelToLoad = MapPath.IsEmpty() ? LevelName : MapPath;
@@ -362,6 +484,21 @@ void APS_Editor_PlayerController::DisableSublevelPostProcessMaterials()
*CameraStart->GetActorLocation().ToString()); *CameraStart->GetActorLocation().ToString());
} }
} }
// Delay 2s then fade in from black over 2 seconds
if (bWaitingForSublevelFadeIn)
{
bWaitingForSublevelFadeIn = false;
FTimerHandle FadeTimer;
GetWorldTimerManager().SetTimer(FadeTimer, [this]()
{
ShowLoadingScreen(false);
if (PlayerCameraManager)
{
PlayerCameraManager->StartCameraFade(1.0f, 0.0f, 2.0f, FLinearColor::Black, false, false);
}
}, 2.0f, false);
}
} }
void APS_Editor_PlayerController::HandleEditorClick(FVector2D ScreenPos, bool bAdditive) void APS_Editor_PlayerController::HandleEditorClick(FVector2D ScreenPos, bool bAdditive)
@@ -389,6 +526,22 @@ void APS_Editor_PlayerController::HandleEditorClick(FVector2D ScreenPos, bool bA
ClickWorldPos = WorldOrigin + WorldDirection * 1000.0f; ClickWorldPos = WorldOrigin + WorldDirection * 1000.0f;
} }
// Snap spline CP to ground: trace downward from click point
{
FHitResult GroundHit;
FCollisionQueryParams GroundParams;
GroundParams.AddIgnoredActor(GetPawn());
if (ActivePlacementActor) GroundParams.AddIgnoredActor(ActivePlacementActor);
const FVector GroundStart = ClickWorldPos + FVector(0.0f, 0.0f, 50.0f);
const FVector GroundEnd = ClickWorldPos - FVector(0.0f, 0.0f, 10000.0f);
if (GetWorld()->LineTraceSingleByChannel(GroundHit, GroundStart, GroundEnd, ECC_Visibility, GroundParams))
{
ClickWorldPos = GroundHit.ImpactPoint + FVector(0.0f, 0.0f, 5.0f);
}
}
if (!ActivePlacementActor) if (!ActivePlacementActor)
{ {
// First click: spawn the actor, track immediately, record undo // First click: spawn the actor, track immediately, record undo
@@ -420,7 +573,7 @@ void APS_Editor_PlayerController::HandleEditorClick(FVector2D ScreenPos, bool bA
} }
// Check if we clicked on the existing spline tube → insert instead of append // Check if we clicked on the existing spline tube → insert instead of append
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(ActivePlacementActor)) if (IPS_Editor_SplineEditable* Spline = Cast<IPS_Editor_SplineEditable>(ActivePlacementActor))
{ {
if (Spline->GetNumPoints() >= 2) if (Spline->GetNumPoints() >= 2)
{ {
@@ -444,7 +597,7 @@ void APS_Editor_PlayerController::HandleEditorClick(FVector2D ScreenPos, bool bA
} }
// Default: add point at end via the interface (with undo) // Default: add point at end via the interface (with undo)
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(ActivePlacementActor)) if (IPS_Editor_SplineEditable* Spline = Cast<IPS_Editor_SplineEditable>(ActivePlacementActor))
{ {
TArray<FVector> OldPoints = Spline->GetAllPointLocations(); TArray<FVector> OldPoints = Spline->GetAllPointLocations();
Spline->AddPoint(ClickWorldPos); Spline->AddPoint(ClickWorldPos);
@@ -452,7 +605,7 @@ void APS_Editor_PlayerController::HandleEditorClick(FVector2D ScreenPos, bool bA
if (UndoManager) if (UndoManager)
{ {
TSharedPtr<FPS_Editor_SplinePointAction> Action = MakeShared<FPS_Editor_SplinePointAction>(); TSharedPtr<FPS_Editor_SplinePointAction> Action = MakeShared<FPS_Editor_SplinePointAction>();
Action->SplineActor = Spline; Action->SplineActor = Cast<AActor>(Spline);
Action->OldPoints = OldPoints; Action->OldPoints = OldPoints;
Action->NewPoints = Spline->GetAllPointLocations(); Action->NewPoints = Spline->GetAllPointLocations();
Action->Description = TEXT("Add spline point"); Action->Description = TEXT("Add spline point");

View File

@@ -12,6 +12,7 @@ class UPS_Editor_SpawnCatalog;
class UPS_Editor_MasterCatalog; class UPS_Editor_MasterCatalog;
class UPS_Editor_SceneSerializer; class UPS_Editor_SceneSerializer;
class APS_Editor_SplineActor; class APS_Editor_SplineActor;
class IPS_Editor_SplineEditable;
class IPS_Editor_PointPlaceable; class IPS_Editor_PointPlaceable;
class ULevelStreamingDynamic; class ULevelStreamingDynamic;
@@ -59,6 +60,20 @@ public:
UFUNCTION(BlueprintCallable, Category = "PS Editor") UFUNCTION(BlueprintCallable, Category = "PS Editor")
void CloseEditor(); void CloseEditor();
// ---- Simulate Mode ----
/** Start simulation: enable AI, store actor positions, disable saving. */
UFUNCTION(BlueprintCallable, Category = "PS Editor")
void StartSimulation();
/** Stop simulation: disable AI, restore actor positions, re-enable saving. */
UFUNCTION(BlueprintCallable, Category = "PS Editor")
void StopSimulation();
/** True while simulation is running. */
UFUNCTION(BlueprintCallable, Category = "PS Editor")
bool IsSimulating() const { return bSimulating; }
/** Load a base level as sublevel for visual context. Unloads previous sublevel. /** Load a base level as sublevel for visual context. Unloads previous sublevel.
* @param LevelName Display name stored in CurrentBaseLevel (used for scene filtering). * @param LevelName Display name stored in CurrentBaseLevel (used for scene filtering).
* @param MapPath Full package path for LoadLevelInstance (e.g. "/Game/Maps/Warehouse"). * @param MapPath Full package path for LoadLevelInstance (e.g. "/Game/Maps/Warehouse").
@@ -91,7 +106,7 @@ public:
bool IsAppendingToActor() const { return bAppendingToActor; } bool IsAppendingToActor() const { return bAppendingToActor; }
// Legacy convenience (for spline-specific code like point editing) // Legacy convenience (for spline-specific code like point editing)
APS_Editor_SplineActor* GetActiveSplineActor() const; IPS_Editor_SplineEditable* GetActiveSplineActor() const;
/** Convenience: enter point placement for an existing spline. */ /** Convenience: enter point placement for an existing spline. */
void BeginSplineEditing(AActor* Spline) { BeginPointPlacementAppend(Spline); } void BeginSplineEditing(AActor* Spline) { BeginPointPlacementAppend(Spline); }
@@ -132,13 +147,28 @@ private:
/** Snapshot of spline points before append, for undo. */ /** Snapshot of spline points before append, for undo. */
TArray<FVector> AppendInitialPoints; TArray<FVector> AppendInitialPoints;
/** True while simulate mode is active (AI running, save disabled). */
bool bSimulating = false;
/** Saved transforms for all spawned actors before simulation, to restore on stop. */
TMap<TWeakObjectPtr<AActor>, FTransform> SimulationSavedTransforms;
/** Currently loaded sublevel instance. */ /** Currently loaded sublevel instance. */
UPROPERTY() UPROPERTY()
TObjectPtr<ULevelStreamingDynamic> CurrentSublevel; TObjectPtr<ULevelStreamingDynamic> CurrentSublevel;
/** True when a sublevel is loading and needs fade-in when ready. */
bool bWaitingForSublevelFadeIn = false;
/** True if we need to apply the fade-to-black once CameraManager is available. */
bool bPendingFadeToBlack = false;
/** Disable custom post-process materials in the loaded sublevel to avoid stencil conflicts. */ /** Disable custom post-process materials in the loaded sublevel to avoid stencil conflicts. */
UFUNCTION() UFUNCTION()
void DisableSublevelPostProcessMaterials(); void DisableSublevelPostProcessMaterials();
/** Show/hide loading screen overlay on the HUD widget. */
void ShowLoadingScreen(bool bShow);
void HandleEditorClick(FVector2D ScreenPos, bool bAdditive); void HandleEditorClick(FVector2D ScreenPos, bool bAdditive);
}; };

View File

@@ -30,7 +30,8 @@ public class PS_Editor : ModuleRules
"Json", "Json",
"JsonUtilities", "JsonUtilities",
"ProceduralMeshComponent", "ProceduralMeshComponent",
"DesktopPlatform" "DesktopPlatform",
"AIModule"
}); });
PrivateDependencyModuleNames.AddRange(new string[] PrivateDependencyModuleNames.AddRange(new string[]

View File

@@ -16,6 +16,7 @@
#include "Materials/MaterialExpressionComponentMask.h" #include "Materials/MaterialExpressionComponentMask.h"
#include "Materials/MaterialExpressionAppendVector.h" #include "Materials/MaterialExpressionAppendVector.h"
#include "Materials/MaterialExpressionVectorParameter.h" #include "Materials/MaterialExpressionVectorParameter.h"
#include "Materials/MaterialExpressionIf.h"
#include "AssetRegistry/AssetRegistryModule.h" #include "AssetRegistry/AssetRegistryModule.h"
#include "UObject/SavePackage.h" #include "UObject/SavePackage.h"
#include "Misc/PackageName.h" #include "Misc/PackageName.h"
@@ -220,6 +221,168 @@ static void CreateOutlineMaterialAsset()
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Selection outline material auto-generated (8-sample, dynamic resolution)")); UE_LOG(LogTemp, Log, TEXT("PS_Editor: Selection outline material auto-generated (8-sample, dynamic resolution)"));
} }
static void CreateSplineOccludedMaterialAsset()
{
const FString MaterialPath = TEXT("/PS_Editor/M_PS_Editor_SplineOccluded");
const FString AssetName = TEXT("M_PS_Editor_SplineOccluded");
if (LoadObject<UMaterial>(nullptr, *(MaterialPath + TEXT(".") + AssetName)))
{
return;
}
UPackage* Package = CreatePackage(*MaterialPath);
UMaterial* Mat = NewObject<UMaterial>(Package, *AssetName, RF_Public | RF_Standalone);
Mat->MaterialDomain = EMaterialDomain::MD_PostProcess;
Mat->BlendableLocation = BL_SceneColorAfterDOF;
auto AddExpr = [&](UMaterialExpression* Expr) { Mat->GetExpressionCollection().AddExpression(Expr); };
// ---- Scene inputs ----
UMaterialExpressionSceneTexture* SceneColor = NewObject<UMaterialExpressionSceneTexture>(Mat);
SceneColor->SceneTextureId = PPI_PostProcessInput0;
AddExpr(SceneColor);
SceneColor->MaterialExpressionEditorX = -400;
SceneColor->MaterialExpressionEditorY = 0;
UMaterialExpressionSceneTexture* CustomStencil = NewObject<UMaterialExpressionSceneTexture>(Mat);
CustomStencil->SceneTextureId = PPI_CustomStencil;
AddExpr(CustomStencil);
CustomStencil->MaterialExpressionEditorX = -400;
CustomStencil->MaterialExpressionEditorY = 200;
UMaterialExpressionSceneTexture* CustomDepthTex = NewObject<UMaterialExpressionSceneTexture>(Mat);
CustomDepthTex->SceneTextureId = PPI_CustomDepth;
AddExpr(CustomDepthTex);
CustomDepthTex->MaterialExpressionEditorX = -400;
CustomDepthTex->MaterialExpressionEditorY = 400;
UMaterialExpressionSceneTexture* SceneDepthTex = NewObject<UMaterialExpressionSceneTexture>(Mat);
SceneDepthTex->SceneTextureId = PPI_SceneDepth;
AddExpr(SceneDepthTex);
SceneDepthTex->MaterialExpressionEditorX = -400;
SceneDepthTex->MaterialExpressionEditorY = 600;
// ---- Stencil == 2 check: abs(stencil - 2) < 0.5 ----
UMaterialExpressionConstant* StencilTarget = NewObject<UMaterialExpressionConstant>(Mat);
StencilTarget->R = 2.0f;
AddExpr(StencilTarget);
StencilTarget->MaterialExpressionEditorX = -200;
StencilTarget->MaterialExpressionEditorY = 200;
UMaterialExpressionSubtract* StencilDiff = NewObject<UMaterialExpressionSubtract>(Mat);
AddExpr(StencilDiff);
StencilDiff->A.Connect(0, CustomStencil);
StencilDiff->B.Connect(0, StencilTarget);
StencilDiff->MaterialExpressionEditorX = 0;
StencilDiff->MaterialExpressionEditorY = 200;
UMaterialExpressionAbs* StencilAbs = NewObject<UMaterialExpressionAbs>(Mat);
AddExpr(StencilAbs);
StencilAbs->Input.Connect(0, StencilDiff);
StencilAbs->MaterialExpressionEditorX = 200;
StencilAbs->MaterialExpressionEditorY = 200;
// Constants for IF outputs
UMaterialExpressionConstant* One = NewObject<UMaterialExpressionConstant>(Mat);
One->R = 1.0f;
AddExpr(One);
One->MaterialExpressionEditorX = 200;
One->MaterialExpressionEditorY = 100;
UMaterialExpressionConstant* Zero = NewObject<UMaterialExpressionConstant>(Mat);
Zero->R = 0.0f;
AddExpr(Zero);
Zero->MaterialExpressionEditorX = 200;
Zero->MaterialExpressionEditorY = 350;
UMaterialExpressionConstant* Half = NewObject<UMaterialExpressionConstant>(Mat);
Half->R = 0.5f;
AddExpr(Half);
Half->MaterialExpressionEditorX = 200;
Half->MaterialExpressionEditorY = 250;
// IF(Half > StencilAbs, 1, 0) → IsSpline
UMaterialExpressionIf* IsSpline = NewObject<UMaterialExpressionIf>(Mat);
AddExpr(IsSpline);
IsSpline->A.Connect(0, Half);
IsSpline->B.Connect(0, StencilAbs);
IsSpline->AGreaterThanB.Connect(0, One);
IsSpline->AEqualsB.Connect(0, Zero);
IsSpline->ALessThanB.Connect(0, Zero);
IsSpline->MaterialExpressionEditorX = 400;
IsSpline->MaterialExpressionEditorY = 200;
// ---- Depth comparison: CustomDepth > SceneDepth + 1 ----
UMaterialExpressionConstant* DepthBias = NewObject<UMaterialExpressionConstant>(Mat);
DepthBias->R = 1.0f;
AddExpr(DepthBias);
DepthBias->MaterialExpressionEditorX = -200;
DepthBias->MaterialExpressionEditorY = 600;
UMaterialExpressionAdd* SceneDepthBiased = NewObject<UMaterialExpressionAdd>(Mat);
AddExpr(SceneDepthBiased);
SceneDepthBiased->A.Connect(0, SceneDepthTex);
SceneDepthBiased->B.Connect(0, DepthBias);
SceneDepthBiased->MaterialExpressionEditorX = 0;
SceneDepthBiased->MaterialExpressionEditorY = 600;
// IF(CustomDepth > SceneDepth+1, 1, 0) → IsOccluded
UMaterialExpressionIf* IsOccluded = NewObject<UMaterialExpressionIf>(Mat);
AddExpr(IsOccluded);
IsOccluded->A.Connect(0, CustomDepthTex);
IsOccluded->B.Connect(0, SceneDepthBiased);
IsOccluded->AGreaterThanB.Connect(0, One);
IsOccluded->AEqualsB.Connect(0, Zero);
IsOccluded->ALessThanB.Connect(0, Zero);
IsOccluded->MaterialExpressionEditorX = 400;
IsOccluded->MaterialExpressionEditorY = 500;
// ---- Final mask = IsSpline * IsOccluded * 0.6 ----
UMaterialExpressionMultiply* CombinedMask = NewObject<UMaterialExpressionMultiply>(Mat);
AddExpr(CombinedMask);
CombinedMask->A.Connect(0, IsSpline);
CombinedMask->B.Connect(0, IsOccluded);
CombinedMask->MaterialExpressionEditorX = 600;
CombinedMask->MaterialExpressionEditorY = 350;
UMaterialExpressionMultiply* FinalMask = NewObject<UMaterialExpressionMultiply>(Mat);
AddExpr(FinalMask);
FinalMask->A.Connect(0, CombinedMask);
FinalMask->ConstB = 0.6f;
FinalMask->MaterialExpressionEditorX = 800;
FinalMask->MaterialExpressionEditorY = 350;
// ---- Gray color ----
UMaterialExpressionConstant4Vector* GrayColor = NewObject<UMaterialExpressionConstant4Vector>(Mat);
GrayColor->Constant = FLinearColor(0.35f, 0.35f, 0.35f, 1.0f);
AddExpr(GrayColor);
GrayColor->MaterialExpressionEditorX = 600;
GrayColor->MaterialExpressionEditorY = 600;
// ---- Lerp(SceneColor, GrayColor, FinalMask) ----
UMaterialExpressionLinearInterpolate* Lerp = NewObject<UMaterialExpressionLinearInterpolate>(Mat);
AddExpr(Lerp);
Lerp->A.Connect(0, SceneColor);
Lerp->B.Connect(0, GrayColor);
Lerp->Alpha.Connect(0, FinalMask);
Lerp->MaterialExpressionEditorX = 1000;
Lerp->MaterialExpressionEditorY = 200;
Mat->GetEditorOnlyData()->EmissiveColor.Connect(0, Lerp);
Mat->PreEditChange(nullptr);
Mat->PostEditChange();
FString PackageFilename = FPackageName::LongPackageNameToFilename(MaterialPath, FPackageName::GetAssetPackageExtension());
FSavePackageArgs SaveArgs;
SaveArgs.TopLevelFlags = RF_Public | RF_Standalone;
UPackage::SavePackage(Package, Mat, *PackageFilename, SaveArgs);
FAssetRegistryModule::AssetCreated(Mat);
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Spline occluded PP material auto-generated (stencil 2, depth compare)"));
}
static void CreateSplineLineMaterialAsset() static void CreateSplineLineMaterialAsset()
{ {
const FString MaterialPath = TEXT("/PS_Editor/M_PS_Editor_SplineLine"); const FString MaterialPath = TEXT("/PS_Editor/M_PS_Editor_SplineLine");
@@ -270,6 +433,7 @@ void FPS_EditorModule::StartupModule()
#if WITH_EDITOR #if WITH_EDITOR
FCoreDelegates::OnPostEngineInit.AddStatic(&CreateOutlineMaterialAsset); FCoreDelegates::OnPostEngineInit.AddStatic(&CreateOutlineMaterialAsset);
FCoreDelegates::OnPostEngineInit.AddStatic(&CreateSplineLineMaterialAsset); FCoreDelegates::OnPostEngineInit.AddStatic(&CreateSplineLineMaterialAsset);
FCoreDelegates::OnPostEngineInit.AddStatic(&CreateSplineOccludedMaterialAsset);
#endif #endif
} }

View File

@@ -6,6 +6,7 @@
#include "PS_Editor_EditableComponent.h" #include "PS_Editor_EditableComponent.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 "GameFramework/PlayerController.h" #include "GameFramework/PlayerController.h"
#include "Engine/World.h" #include "Engine/World.h"
#include "GameFramework/Pawn.h" #include "GameFramework/Pawn.h"
@@ -188,7 +189,7 @@ void UPS_Editor_SelectionManager::SelectActor(AActor* Actor)
OnSelectionChanged.Broadcast(); OnSelectionChanged.Broadcast();
// Mark spline as selected (handles shown only in SplineEditing mode, not here) // Mark spline as selected (handles shown only in SplineEditing mode, not here)
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(Actor)) if (IPS_Editor_SplineEditable* Spline = Cast<IPS_Editor_SplineEditable>(Actor))
{ {
Spline->SetSelected(true); Spline->SetSelected(true);
} }
@@ -215,7 +216,7 @@ void UPS_Editor_SelectionManager::DeselectActor(AActor* Actor)
OnSelectionChanged.Broadcast(); OnSelectionChanged.Broadcast();
// Unmark spline // Unmark spline
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(Actor)) if (IPS_Editor_SplineEditable* Spline = Cast<IPS_Editor_SplineEditable>(Actor))
{ {
Spline->SetHandlesVisible(false); Spline->SetHandlesVisible(false);
Spline->SetSelected(false); Spline->SetSelected(false);
@@ -258,7 +259,7 @@ void UPS_Editor_SelectionManager::ClearSelection()
SetActorHighlight(Actor, false); SetActorHighlight(Actor, false);
// Spline-specific cleanup // Spline-specific cleanup
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(Actor)) if (IPS_Editor_SplineEditable* Spline = Cast<IPS_Editor_SplineEditable>(Actor))
{ {
Spline->SetHandlesVisible(false); Spline->SetHandlesVisible(false);
Spline->SetSelected(false); Spline->SetSelected(false);
@@ -316,7 +317,7 @@ FVector UPS_Editor_SelectionManager::GetSelectionPivot() const
if (AActor* Actor = Weak.Get()) if (AActor* Actor = Weak.Get())
{ {
// For SplineActors, use the centroid of all points as pivot // For SplineActors, use the centroid of all points as pivot
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(Actor)) if (IPS_Editor_SplineEditable* Spline = Cast<IPS_Editor_SplineEditable>(Actor))
{ {
Sum += Spline->GetCentroid(); Sum += Spline->GetCentroid();
} }
@@ -523,7 +524,7 @@ void UPS_Editor_SelectionManager::UpdateGizmo()
// Override pivot for SplineActors: use centroid so gizmo aligns with center cube // Override pivot for SplineActors: use centroid so gizmo aligns with center cube
if (SelectedActors.Num() == 1) if (SelectedActors.Num() == 1)
{ {
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(SelectedActors[0].Get())) if (IPS_Editor_SplineEditable* Spline = Cast<IPS_Editor_SplineEditable>(SelectedActors[0].Get()))
{ {
Pivot = Spline->GetCentroid(); Pivot = Spline->GetCentroid();
} }

View File

@@ -1,11 +1,11 @@
#include "PS_Editor_UndoManager.h" #include "PS_Editor_UndoManager.h"
#include "PS_Editor_SplineActor.h" #include "PS_Editor_SplineEditable.h"
#include "UObject/UnrealType.h" #include "UObject/UnrealType.h"
#include "Components/ActorComponent.h" #include "Components/ActorComponent.h"
void FPS_Editor_SplinePointAction::Execute() void FPS_Editor_SplinePointAction::Execute()
{ {
if (APS_Editor_SplineActor* Spline = SplineActor.Get()) if (IPS_Editor_SplineEditable* Spline = Cast<IPS_Editor_SplineEditable>(SplineActor.Get()))
{ {
Spline->SetAllPointLocations(NewPoints); Spline->SetAllPointLocations(NewPoints);
} }
@@ -13,7 +13,7 @@ void FPS_Editor_SplinePointAction::Execute()
void FPS_Editor_SplinePointAction::Undo() void FPS_Editor_SplinePointAction::Undo()
{ {
if (APS_Editor_SplineActor* Spline = SplineActor.Get()) if (IPS_Editor_SplineEditable* Spline = Cast<IPS_Editor_SplineEditable>(SplineActor.Get()))
{ {
Spline->SetAllPointLocations(OldPoints); Spline->SetAllPointLocations(OldPoints);
} }

View File

@@ -158,17 +158,16 @@ struct FPS_Editor_SpawnAction : public FPS_Editor_Action
} }
}; };
class APS_Editor_SplineActor;
/** /**
* Records a spline point state change (move, add, remove). * Records a spline point state change (move, add, remove).
* Stores full state snapshots (all points before/after) for simplicity. * Stores full state snapshots (all points before/after) for simplicity.
* SplineActor must implement IPS_Editor_SplineEditable.
*/ */
struct FPS_Editor_SplinePointAction : public FPS_Editor_Action struct FPS_Editor_SplinePointAction : public FPS_Editor_Action
{ {
FPS_Editor_SplinePointAction() : FPS_Editor_Action(EPS_Editor_ActionType::SplinePointChange) {} FPS_Editor_SplinePointAction() : FPS_Editor_Action(EPS_Editor_ActionType::SplinePointChange) {}
TWeakObjectPtr<APS_Editor_SplineActor> SplineActor; TWeakObjectPtr<AActor> SplineActor;
TArray<FVector> OldPoints; TArray<FVector> OldPoints;
TArray<FVector> NewPoints; TArray<FVector> NewPoints;
FString Description; FString Description;

View File

@@ -4,6 +4,7 @@
#include "PS_Editor_EditableInterface.h" #include "PS_Editor_EditableInterface.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_PointPlaceable.h" #include "PS_Editor_PointPlaceable.h"
#include "JsonObjectConverter.h" #include "JsonObjectConverter.h"
#include "Misc/FileHelper.h" #include "Misc/FileHelper.h"
@@ -74,6 +75,12 @@ bool UPS_Editor_SceneLoader::LoadScene(const UObject* WorldContextObject, const
{ {
Comp->DestroyComponent(); Comp->DestroyComponent();
} }
// Hide spline visualization in gameplay (tube, handles) — data stays for AI
if (IPS_Editor_SplineEditable* Spline = Cast<IPS_Editor_SplineEditable>(Actor))
{
Spline->SetVisualizationVisible(false);
}
} }
} }
@@ -144,7 +151,7 @@ AActor* UPS_Editor_SceneLoader::SpawnActorFromData(UWorld* World, const FPS_Edit
} }
// Import spline data if applicable // Import spline data if applicable
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(SpawnedActor)) if (IPS_Editor_SplineEditable* Spline = Cast<IPS_Editor_SplineEditable>(SpawnedActor))
{ {
Spline->ImportFromCustomProperties(ActorData.CustomProperties); Spline->ImportFromCustomProperties(ActorData.CustomProperties);
} }

View File

@@ -6,6 +6,9 @@
#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"
#include "AIController.h"
#include "BrainComponent.h"
#include "PS_Editor_SplineEditable.h"
#include "JsonObjectConverter.h" #include "JsonObjectConverter.h"
#include "Misc/FileHelper.h" #include "Misc/FileHelper.h"
#include "Misc/Paths.h" #include "Misc/Paths.h"
@@ -47,7 +50,7 @@ bool UPS_Editor_SceneSerializer::SaveScene(const FString& SceneName, UPS_Editor_
ActorData.Scale = Actor->GetActorScale3D(); ActorData.Scale = Actor->GetActorScale3D();
// Export spline data // Export spline data
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(Actor)) if (IPS_Editor_SplineEditable* Spline = Cast<IPS_Editor_SplineEditable>(Actor))
{ {
Spline->ExportToCustomProperties(ActorData.CustomProperties); Spline->ExportToCustomProperties(ActorData.CustomProperties);
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Exported spline with %d points, class=%s"), UE_LOG(LogTemp, Log, TEXT("PS_Editor: Exported spline with %d points, class=%s"),
@@ -214,7 +217,7 @@ bool UPS_Editor_SceneSerializer::LoadScene(const FString& SceneName, UPS_Editor_
} }
// Import spline data // Import spline data
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(SpawnedActor)) if (IPS_Editor_SplineEditable* Spline = Cast<IPS_Editor_SplineEditable>(SpawnedActor))
{ {
Spline->ImportFromCustomProperties(ActorData.CustomProperties); Spline->ImportFromCustomProperties(ActorData.CustomProperties);
} }
@@ -226,6 +229,25 @@ bool UPS_Editor_SceneSerializer::LoadScene(const FString& SceneName, UPS_Editor_
IPS_Editor_EditableInterface::Execute_OnEditorModeChanged(SpawnedActor, true); IPS_Editor_EditableInterface::Execute_OnEditorModeChanged(SpawnedActor, true);
} }
// Stop AI on spawned pawns (editor mode — AI only runs during Simulate)
if (APawn* P = Cast<APawn>(SpawnedActor))
{
TWeakObjectPtr<APawn> WeakP = P;
P->GetWorldTimerManager().SetTimerForNextTick([WeakP]()
{
if (APawn* Pawn = WeakP.Get())
{
if (AAIController* AIC = Cast<AAIController>(Pawn->GetController()))
{
if (UBrainComponent* Brain = AIC->GetBrainComponent())
{
Brain->StopLogic(TEXT("PS_Editor"));
}
}
}
});
}
SpawnedCount++; SpawnedCount++;
} }
} }

View File

@@ -4,6 +4,8 @@
#include "PS_Editor_EditableInterface.h" #include "PS_Editor_EditableInterface.h"
#include "PS_Editor_UndoManager.h" #include "PS_Editor_UndoManager.h"
#include "PS_Editor_PlayerController.h" #include "PS_Editor_PlayerController.h"
#include "AIController.h"
#include "BrainComponent.h"
#include "GameFramework/PlayerController.h" #include "GameFramework/PlayerController.h"
#include "Engine/World.h" #include "Engine/World.h"
@@ -166,6 +168,31 @@ AActor* UPS_Editor_SpawnManager::SpawnFromCatalogAtLocation(int32 Index, FVector
IPS_Editor_EditableInterface::Execute_OnEditorModeChanged(SpawnedActor, true); IPS_Editor_EditableInterface::Execute_OnEditorModeChanged(SpawnedActor, true);
} }
// Stop AI logic on spawned pawns in editor mode (deferred — AIController may not be assigned yet)
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(PC))
{
if (!EditorPC->IsSimulating())
{
if (APawn* SpawnedPawn = Cast<APawn>(SpawnedActor))
{
TWeakObjectPtr<APawn> WeakPawn = SpawnedPawn;
SpawnedPawn->GetWorldTimerManager().SetTimerForNextTick([WeakPawn]()
{
if (APawn* P = WeakPawn.Get())
{
if (AAIController* AIC = Cast<AAIController>(P->GetController()))
{
if (UBrainComponent* Brain = AIC->GetBrainComponent())
{
Brain->StopLogic(TEXT("PS_Editor"));
}
}
}
});
}
}
}
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Spawned %s at %s"), *Entry.DisplayName, *WorldLocation.ToString()); UE_LOG(LogTemp, Log, TEXT("PS_Editor: Spawned %s at %s"), *Entry.DisplayName, *WorldLocation.ToString());
return SpawnedActor; return SpawnedActor;
} }

View File

@@ -30,6 +30,8 @@ APS_Editor_SplineActor::APS_Editor_SplineActor()
SplineMeshComp->SetCollisionResponseToChannel(ECC_Camera, ECR_Block); SplineMeshComp->SetCollisionResponseToChannel(ECC_Camera, ECR_Block);
SplineMeshComp->bUseComplexAsSimpleCollision = true; SplineMeshComp->bUseComplexAsSimpleCollision = true;
SplineMeshComp->SetCastShadow(false); SplineMeshComp->SetCastShadow(false);
SplineMeshComp->SetRenderCustomDepth(true);
SplineMeshComp->SetCustomDepthStencilValue(2);
// Handle parent // Handle parent
HandleRoot = CreateDefaultSubobject<USceneComponent>(TEXT("HandleRoot")); HandleRoot = CreateDefaultSubobject<USceneComponent>(TEXT("HandleRoot"));
@@ -87,6 +89,10 @@ void APS_Editor_SplineActor::BeginPlay()
CenterCubeMat = UMaterialInstanceDynamic::Create(GizmoMat, this); CenterCubeMat = UMaterialInstanceDynamic::Create(GizmoMat, this);
CenterCubeMat->SetVectorParameterValue(TEXT("Color"), SplineColor); CenterCubeMat->SetVectorParameterValue(TEXT("Color"), SplineColor);
// Occluded pass: gray, no depth test — visible through walls
SplineMatOccluded = UMaterialInstanceDynamic::Create(GizmoMat, this);
SplineMatOccluded->SetVectorParameterValue(TEXT("Color"), FLinearColor(0.3f, 0.3f, 0.3f));
} }
// Create center cube (selection handle for whole spline) // Create center cube (selection handle for whole spline)
@@ -255,14 +261,12 @@ void APS_Editor_SplineActor::UpdateVisualization()
} }
SplineMeshComp->ClearAllMeshSections(); SplineMeshComp->ClearAllMeshSections();
SplineMeshComp->CreateMeshSection(0, Vertices, Triangles, Normals, UVs, TArray<FColor>(), TArray<FProcMeshTangent>(), true); SplineMeshComp->CreateMeshSection(0, Vertices, Triangles, Normals, UVs, TArray<FColor>(), TArray<FProcMeshTangent>(), true);
// Force collision and bounds update immediately
SplineMeshComp->UpdateBounds(); SplineMeshComp->UpdateBounds();
SplineMeshComp->MarkRenderStateDirty(); SplineMeshComp->MarkRenderStateDirty();
// Apply the current material (normal or selected)
// Check if handles are visible as a proxy for "selected" state
const bool bCurrentlySelected = (PointHandles.Num() > 0 && PointHandles[0] && PointHandles[0]->IsVisible()); const bool bCurrentlySelected = (PointHandles.Num() > 0 && PointHandles[0] && PointHandles[0]->IsVisible());
UMaterialInstanceDynamic* LineMat = (bCurrentlySelected && SplineMatSelected) ? SplineMatSelected : SplineMat; UMaterialInstanceDynamic* LineMat = (bCurrentlySelected && SplineMatSelected) ? SplineMatSelected : SplineMat;
if (LineMat) if (LineMat)
@@ -419,15 +423,12 @@ void APS_Editor_SplineActor::SetSelected(bool bSelected)
if (SplineMeshComp) if (SplineMeshComp)
{ {
UMaterialInstanceDynamic* Mat = bSelected ? SplineMatSelected : SplineMat; UMaterialInstanceDynamic* Mat = bSelected ? SplineMatSelected : SplineMat;
UE_LOG(LogTemp, Log, TEXT("PS_Editor: SetSelected(%s) - SplineMat=%s, SplineMatSelected=%s, Using=%s"),
bSelected ? TEXT("true") : TEXT("false"),
SplineMat ? TEXT("valid") : TEXT("NULL"),
SplineMatSelected ? TEXT("valid") : TEXT("NULL"),
Mat ? TEXT("valid") : TEXT("NULL"));
if (Mat) if (Mat)
{ {
SplineMeshComp->SetMaterial(0, Mat); SplineMeshComp->SetMaterial(0, Mat);
} }
// Restore stencil 2 for occluded PP effect when deselected (selection sets it to 1)
SplineMeshComp->SetCustomDepthStencilValue(bSelected ? 1 : 2);
} }
// Center cube: selected color when selected, normal color otherwise // Center cube: selected color when selected, normal color otherwise
@@ -458,6 +459,21 @@ void APS_Editor_SplineActor::UpdateSplineMaterials()
} }
} }
void APS_Editor_SplineActor::SetVisualizationVisible(bool bVisible)
{
if (SplineMeshComp) SplineMeshComp->SetVisibility(bVisible);
if (CenterCube) CenterCube->SetVisibility(bVisible);
if (HandleRoot) HandleRoot->SetVisibility(bVisible, true);
for (UStaticMeshComponent* Handle : PointHandles)
{
if (Handle)
{
Handle->SetVisibility(bVisible);
Handle->SetCollisionEnabled(bVisible ? ECollisionEnabled::QueryOnly : ECollisionEnabled::NoCollision);
}
}
}
bool APS_Editor_SplineActor::IsSplineMesh(const UPrimitiveComponent* Comp) const bool APS_Editor_SplineActor::IsSplineMesh(const UPrimitiveComponent* Comp) const
{ {
return Comp && Comp == SplineMeshComp; return Comp && Comp == SplineMeshComp;

View File

@@ -3,6 +3,7 @@
#include "CoreMinimal.h" #include "CoreMinimal.h"
#include "GameFramework/Actor.h" #include "GameFramework/Actor.h"
#include "PS_Editor_PointPlaceable.h" #include "PS_Editor_PointPlaceable.h"
#include "PS_Editor_SplineEditable.h"
#include "PS_Editor_SplineActor.generated.h" #include "PS_Editor_SplineActor.generated.h"
class USplineComponent; class USplineComponent;
@@ -15,7 +16,7 @@ class UPS_Editor_EditableComponent;
* Renders as a visible quad strip with clickable point handles. * Renders as a visible quad strip with clickable point handles.
*/ */
UCLASS() UCLASS()
class PS_EDITOR_API APS_Editor_SplineActor : public AActor, public IPS_Editor_PointPlaceable class PS_EDITOR_API APS_Editor_SplineActor : public AActor, public IPS_Editor_PointPlaceable, public IPS_Editor_SplineEditable
{ {
GENERATED_BODY() GENERATED_BODY()
@@ -46,74 +47,77 @@ public:
// ---- Point manipulation ---- // ---- Point manipulation ----
/** Add a point at the end of the spline (world space). */ /** Add a point at the end of the spline (world space). */
void AddPoint(const FVector& WorldPosition); virtual void AddPoint(const FVector& WorldPosition) override;
/** Move an existing point to a new world position. */ /** Move an existing point to a new world position. */
void MovePoint(int32 Index, const FVector& NewWorldPosition); virtual void MovePoint(int32 Index, const FVector& NewWorldPosition) override;
/** Remove a point by index. */ /** Remove a point by index. */
void RemovePoint(int32 Index); virtual void RemovePoint(int32 Index) override;
/** Get the number of spline points. */ /** Get the number of spline points. */
int32 GetNumPoints() const; virtual int32 GetNumPoints() const override;
/** Get the world position of a spline point. */ /** Get the world position of a spline point. */
FVector GetPointLocation(int32 Index) const; virtual FVector GetPointLocation(int32 Index) const override;
/** Get all point world positions (for undo snapshots). */ /** Get all point world positions (for undo snapshots). */
TArray<FVector> GetAllPointLocations() const; virtual TArray<FVector> GetAllPointLocations() const override;
/** Set all point positions at once (for undo restore). Rebuilds handles and visualization. */ /** Set all point positions at once (for undo restore). Rebuilds handles and visualization. */
void SetAllPointLocations(const TArray<FVector>& Points); virtual void SetAllPointLocations(const TArray<FVector>& Points) override;
// ---- Visualization ---- // ---- Visualization ----
/** Regenerate the line mesh and reposition all handles. */ /** Regenerate the line mesh and reposition all handles. */
void UpdateVisualization(); virtual void UpdateVisualization() override;
/** Re-apply SplineColor to materials (call after changing SplineColor at runtime). */ /** Re-apply SplineColor to materials (call after changing SplineColor at runtime). */
UFUNCTION(BlueprintCallable, Category = "PS Editor|Spline") UFUNCTION(BlueprintCallable, Category = "PS Editor|Spline")
void UpdateSplineMaterials(); virtual void UpdateSplineMaterials() override;
// ---- Handle interaction ---- // ---- Handle interaction ----
/** Find which point handle is under the cursor ray. Returns -1 if none. */ /** Find which point handle is under the cursor ray. Returns -1 if none. */
int32 GetHandleIndexUnderCursor(const FVector& RayOrigin, const FVector& RayDirection, float SphereRadius = 15.0f) const; virtual int32 GetHandleIndexUnderCursor(const FVector& RayOrigin, const FVector& RayDirection, float SphereRadius = 15.0f) const override;
/** Show or hide all point handles and center cube. */ /** Show or hide all point handles and center cube. */
void SetHandlesVisible(bool bVisible); virtual void SetHandlesVisible(bool bVisible) override;
/** Set the spline's selected state (changes line + cube color). */ /** Set the spline's selected state (changes line + cube color). */
void SetSelected(bool bSelected); virtual void SetSelected(bool bSelected) override;
/** Returns true if the given component is the center cube. */ /** Returns true if the given component is the center cube. */
bool IsCenterCube(const UPrimitiveComponent* Comp) const; virtual bool IsCenterCube(const UPrimitiveComponent* Comp) const override;
/** Highlight a specific handle (yellow), others go back to normal (white). */ /** Highlight a specific handle (yellow), others go back to normal (white). */
void HighlightHandle(int32 Index); virtual void HighlightHandle(int32 Index) override;
/** Clear all handle highlights. */ /** Clear all handle highlights. */
void ClearHandleHighlight(); virtual void ClearHandleHighlight() override;
// ---- Serialization ---- // ---- Serialization ----
/** Returns true if the given component is the spline line mesh. */ /** Returns true if the given component is the spline line mesh. */
bool IsSplineMesh(const UPrimitiveComponent* Comp) const; virtual bool IsSplineMesh(const UPrimitiveComponent* Comp) const override;
/** Find the best insertion index for a new point at the given world location. */ /** Find the best insertion index for a new point at the given world location. */
int32 FindInsertIndex(const FVector& WorldLocation) const; virtual int32 FindInsertIndex(const FVector& WorldLocation) const override;
/** Insert a point at a specific index. */ /** Insert a point at a specific index. */
void InsertPoint(int32 Index, const FVector& WorldPosition); virtual void InsertPoint(int32 Index, const FVector& WorldPosition) override;
/** Get the centroid of all spline points (world space). Used as pivot for gizmo. */ /** Get the centroid of all spline points (world space). Used as pivot for gizmo. */
FVector GetCentroid() const; virtual FVector GetCentroid() const override;
/** Hide/show the spline visualization. */
virtual void SetVisualizationVisible(bool bVisible) override;
/** Export spline data to a key-value map (for JSON serialization). */ /** Export spline data to a key-value map (for JSON serialization). */
void ExportToCustomProperties(TMap<FString, FString>& OutProperties) const; virtual void ExportToCustomProperties(TMap<FString, FString>& OutProperties) const override;
/** Import spline data from a key-value map (for scene loading). */ /** Import spline data from a key-value map (for scene loading). */
void ImportFromCustomProperties(const TMap<FString, FString>& Properties); virtual void ImportFromCustomProperties(const TMap<FString, FString>& Properties) override;
protected: protected:
virtual void BeginPlay() override; virtual void BeginPlay() override;
@@ -144,6 +148,9 @@ private:
UPROPERTY() UPROPERTY()
TObjectPtr<UMaterialInstanceDynamic> HandleHighlightMat; TObjectPtr<UMaterialInstanceDynamic> HandleHighlightMat;
UPROPERTY()
TObjectPtr<UMaterialInstanceDynamic> SplineMatOccluded; // Gray, no depth test — visible through walls
UPROPERTY() UPROPERTY()
TObjectPtr<UStaticMeshComponent> CenterCube; TObjectPtr<UStaticMeshComponent> CenterCube;

View File

@@ -0,0 +1,63 @@
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "PS_Editor_SplineEditable.generated.h"
class USplineComponent;
UINTERFACE(BlueprintType, meta = (DisplayName = "PS Editor Spline Editable"))
class PS_EDITOR_API UPS_Editor_SplineEditable : public UInterface
{
GENERATED_BODY()
};
/**
* Interface for any actor that provides spline editing in PS_Editor.
* Implemented by APS_Editor_SplineActor and APS_BehaviorEditor_AISpline.
*
* The editor (Pawn, SelectionManager, Serializer, etc.) casts to this interface
* instead of a concrete class, so any spline-like actor works with the full
* editing flow: point placement, handle drag, insert, delete, undo, serialization.
*/
class PS_EDITOR_API IPS_Editor_SplineEditable
{
GENERATED_BODY()
public:
// ---- Point manipulation ----
virtual void AddPoint(const FVector& WorldPosition) = 0;
virtual void MovePoint(int32 Index, const FVector& NewWorldPosition) = 0;
virtual void RemovePoint(int32 Index) = 0;
virtual int32 GetNumPoints() const = 0;
virtual FVector GetPointLocation(int32 Index) const = 0;
virtual TArray<FVector> GetAllPointLocations() const = 0;
virtual void SetAllPointLocations(const TArray<FVector>& Points) = 0;
virtual void InsertPoint(int32 Index, const FVector& WorldPosition) = 0;
virtual int32 FindInsertIndex(const FVector& WorldLocation) const = 0;
virtual FVector GetCentroid() const = 0;
// ---- Visualization ----
virtual void UpdateVisualization() = 0;
virtual void UpdateSplineMaterials() = 0;
// ---- Handle interaction ----
virtual int32 GetHandleIndexUnderCursor(const FVector& RayOrigin, const FVector& RayDirection, float SphereRadius = 15.0f) const = 0;
virtual void SetHandlesVisible(bool bVisible) = 0;
virtual void SetSelected(bool bSelected) = 0;
virtual bool IsCenterCube(const UPrimitiveComponent* Comp) const = 0;
virtual bool IsSplineMesh(const UPrimitiveComponent* Comp) const = 0;
virtual void HighlightHandle(int32 Index) = 0;
virtual void ClearHandleHighlight() = 0;
// ---- Serialization ----
virtual void ExportToCustomProperties(TMap<FString, FString>& OutProperties) const = 0;
virtual void ImportFromCustomProperties(const TMap<FString, FString>& Properties) = 0;
// ---- Visibility ----
/** Hide/show the spline visualization (tube, handles, center cube). Data remains for AI. */
virtual void SetVisualizationVisible(bool bVisible) = 0;
// ---- Access ----
virtual USplineComponent* GetSplineComponent() const = 0;
};

View File

@@ -9,6 +9,7 @@
#include "PS_Editor_EditableInterface.h" #include "PS_Editor_EditableInterface.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_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"
@@ -45,6 +46,27 @@ void UPS_Editor_MainWidget::SetSceneSerializer(UPS_Editor_SceneSerializer* InSer
TSharedRef<SWidget> UPS_Editor_MainWidget::RebuildWidget() TSharedRef<SWidget> UPS_Editor_MainWidget::RebuildWidget()
{ {
return SNew(SOverlay) return SNew(SOverlay)
// Loading screen (behind everything — first slot = bottom layer)
+ SOverlay::Slot()
.HAlign(HAlign_Fill)
.VAlign(VAlign_Fill)
[
SAssignNew(LoadingOverlay, SBorder)
.Visibility(EVisibility::Visible)
.BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush"))
.BorderBackgroundColor(FLinearColor(0.0f, 0.0f, 0.0f, 1.0f))
[
SNew(SBox)
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
[
SNew(STextBlock)
.Text(FText::FromString(TEXT("Loading...")))
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 18))
.ColorAndOpacity(FLinearColor(0.7f, 0.7f, 0.7f, 1.0f))
]
]
]
// Top-left: Title + Toolbar // Top-left: Title + Toolbar
+ SOverlay::Slot() + SOverlay::Slot()
.HAlign(HAlign_Left) .HAlign(HAlign_Left)
@@ -183,9 +205,9 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::RebuildWidget()
{ {
for (const TWeakObjectPtr<AActor>& Weak : SelMgr->GetSelectedActors()) for (const TWeakObjectPtr<AActor>& Weak : SelMgr->GetSelectedActors())
{ {
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(Weak.Get())) if (Cast<IPS_Editor_SplineEditable>(Weak.Get()))
{ {
EditorPC->BeginSplineEditing(Spline); EditorPC->BeginSplineEditing(Weak.Get());
break; break;
} }
} }
@@ -563,6 +585,19 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
SNew(SButton) SNew(SButton)
.OnClicked_Lambda([this]() -> FReply .OnClicked_Lambda([this]() -> FReply
{ {
// Block save during simulation
if (APS_Editor_PlayerController* SimPC = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
{
if (SimPC->IsSimulating())
{
if (HelpBarText.IsValid())
{
HelpBarText->SetText(FText::FromString(TEXT("Cannot save during simulation. Stop simulation first.")));
HelpBarText->SetColorAndOpacity(FLinearColor(1.0f, 0.4f, 0.2f));
}
return FReply::Handled();
}
}
if (SaveNameField.IsValid() && SceneSerializer.IsValid()) if (SaveNameField.IsValid() && SceneSerializer.IsValid())
{ {
FString Name = SaveNameField->GetText().ToString(); FString Name = SaveNameField->GetText().ToString();
@@ -692,26 +727,42 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
// Store scene name in GameInstance subsystem (survives OpenLevel) // Store scene name in GameInstance subsystem (survives OpenLevel)
FString BaseLevel = EditorPC->CurrentBaseLevel; FString BaseLevel = EditorPC->CurrentBaseLevel;
UE_LOG(LogTemp, Warning, TEXT("PS_Editor Play: BaseLevel='%s'"), *BaseLevel); FString SceneName = SaveNameField.IsValid() ? SaveNameField->GetText().ToString() : TEXT("");
if (BaseLevel.IsEmpty())
// Resolve map path: BaseLevel from catalog, or current map if none selected
FString MapPath;
if (!BaseLevel.IsEmpty())
{ {
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: No BaseLevel set, cannot Play.")); if (FString* Found = BaseLevelPathMap.Find(BaseLevel))
return FReply::Handled(); {
MapPath = *Found;
}
else
{
MapPath = FString::Printf(TEXT("/Game/%s"), *BaseLevel);
}
}
else
{
// No BaseLevel = play on the current editor map
MapPath = EditorPC->GetWorld()->GetMapName();
MapPath.RemoveFromStart(EditorPC->GetWorld()->StreamingLevelsPrefix);
BaseLevel = MapPath;
} }
FString SceneName = SaveNameField.IsValid() ? SaveNameField->GetText().ToString() : TEXT(""); UE_LOG(LogTemp, Log, TEXT("PS_Editor Play: BaseLevel='%s', Map='%s', Scenario='%s'"), *BaseLevel, *MapPath, *SceneName);
if (UGameInstance* GI = EditorPC->GetGameInstance()) if (UGameInstance* GI = EditorPC->GetGameInstance())
{ {
if (UPS_Editor_GameInstanceSubsystem* Sub = GI->GetSubsystem<UPS_Editor_GameInstanceSubsystem>()) if (UPS_Editor_GameInstanceSubsystem* Sub = GI->GetSubsystem<UPS_Editor_GameInstanceSubsystem>())
{ {
Sub->SetPendingScenario(SceneName, BaseLevel); Sub->SetPendingScenario(SceneName, BaseLevel);
Sub->bIsPlayMode = true; Sub->bIsPlayMode = true;
// Remember editor map for ReturnToEditor
if (Sub->EditorMapName.IsEmpty()) if (Sub->EditorMapName.IsEmpty())
{ {
FString MapName = EditorPC->GetWorld()->GetMapName(); FString EdMapName = EditorPC->GetWorld()->GetMapName();
MapName.RemoveFromStart(EditorPC->GetWorld()->StreamingLevelsPrefix); EdMapName.RemoveFromStart(EditorPC->GetWorld()->StreamingLevelsPrefix);
Sub->EditorMapName = MapName; Sub->EditorMapName = EdMapName;
} }
} }
} }
@@ -729,16 +780,6 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
EditorPC->bShowMouseCursor = false; EditorPC->bShowMouseCursor = false;
EditorPC->SetInputMode(FInputModeGameOnly()); EditorPC->SetInputMode(FInputModeGameOnly());
// Use full path from MasterCatalog, fallback to /Game/{name}
FString MapPath;
if (FString* Found = BaseLevelPathMap.Find(BaseLevel))
{
MapPath = *Found;
}
else
{
MapPath = FString::Printf(TEXT("/Game/%s"), *BaseLevel);
}
UGameplayStatics::OpenLevel(EditorPC, FName(*MapPath), true, TEXT("")); UGameplayStatics::OpenLevel(EditorPC, FName(*MapPath), true, TEXT(""));
return FReply::Handled(); return FReply::Handled();
}) })
@@ -752,6 +793,33 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
+ SHorizontalBox::Slot() + SHorizontalBox::Slot()
.AutoWidth() .AutoWidth()
.Padding(4.0f, 0.0f, 0.0f, 0.0f) .Padding(4.0f, 0.0f, 0.0f, 0.0f)
[
SNew(SButton)
.OnClicked_Lambda([this]() -> FReply
{
APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetOwningPlayer());
if (!EditorPC) return FReply::Handled();
if (EditorPC->IsSimulating())
{
EditorPC->StopSimulation();
}
else
{
EditorPC->StartSimulation();
}
return FReply::Handled();
})
[
SAssignNew(SimulateButtonText, STextBlock)
.Text(FText::FromString(TEXT("Simulate")))
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 10))
.ColorAndOpacity(FLinearColor(1.0f, 0.6f, 0.0f))
]
]
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(4.0f, 0.0f, 0.0f, 0.0f)
[ [
SNew(SButton) SNew(SButton)
.OnClicked_Lambda([this]() -> FReply .OnClicked_Lambda([this]() -> FReply
@@ -999,6 +1067,19 @@ void UPS_Editor_MainWidget::NativeConstruct()
} }
PopulateSceneList(); PopulateSceneList();
// Hide loading overlay if no sublevel is being loaded
if (APS_Editor_PlayerController* PC = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
{
if (PC->CurrentBaseLevel.IsEmpty())
{
ShowLoadingScreen(false);
}
}
else
{
ShowLoadingScreen(false);
}
} }
void UPS_Editor_MainWidget::PopulateSceneList() void UPS_Editor_MainWidget::PopulateSceneList()
@@ -1272,6 +1353,24 @@ void UPS_Editor_MainWidget::NativeTick(const FGeometry& MyGeometry, float InDelt
} }
} }
// Update simulate button text
if (SimulateButtonText.IsValid())
{
if (APS_Editor_PlayerController* SimPC = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
{
if (SimPC->IsSimulating())
{
SimulateButtonText->SetText(FText::FromString(TEXT("Stop")));
SimulateButtonText->SetColorAndOpacity(FLinearColor(1.0f, 0.3f, 0.2f));
}
else
{
SimulateButtonText->SetText(FText::FromString(TEXT("Simulate")));
SimulateButtonText->SetColorAndOpacity(FLinearColor(1.0f, 0.6f, 0.0f));
}
}
}
RefreshPropertiesFromSelection(); RefreshPropertiesFromSelection();
RefreshOutliner(); RefreshOutliner();
} }
@@ -1319,7 +1418,7 @@ void UPS_Editor_MainWidget::RefreshPropertiesFromSelection()
{ {
for (const TWeakObjectPtr<AActor>& Weak : SM->GetSelectedActors()) for (const TWeakObjectPtr<AActor>& Weak : SM->GetSelectedActors())
{ {
if (Cast<APS_Editor_SplineActor>(Weak.Get())) if (Cast<IPS_Editor_SplineEditable>(Weak.Get()))
{ {
bSplineSelected = true; bSplineSelected = true;
break; break;
@@ -1564,11 +1663,7 @@ void UPS_Editor_MainWidget::RefreshOutliner()
// Build display name // Build display name
FString DisplayName; FString DisplayName;
if (Cast<APS_Editor_SplineActor>(Actor)) if (UPS_Editor_SpawnableComponent* SC = Actor->FindComponentByClass<UPS_Editor_SpawnableComponent>())
{
DisplayName = TEXT("Spline");
}
else if (UPS_Editor_SpawnableComponent* SC = Actor->FindComponentByClass<UPS_Editor_SpawnableComponent>())
{ {
if (!SC->DisplayName.IsEmpty()) if (!SC->DisplayName.IsEmpty())
{ {
@@ -2084,10 +2179,28 @@ void UPS_Editor_MainWidget::ApplyPropertyFromUI(int32 RowIndex)
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: ApplyProperty '%s' = '%s' -> Import %s"), UE_LOG(LogTemp, Warning, TEXT("PS_Editor: ApplyProperty '%s' = '%s' -> Import %s"),
*Row.Key, *NewValueText, ImportResult ? TEXT("OK") : TEXT("FAILED")); *Row.Key, *NewValueText, ImportResult ? TEXT("OK") : TEXT("FAILED"));
// Refresh rendering // Refresh rendering + force network replication (triggers OnRep on clients)
if (UActorComponent* Comp = Cast<UActorComponent>(Target)) if (UActorComponent* Comp = Cast<UActorComponent>(Target))
{ {
Comp->MarkRenderStateDirty(); Comp->MarkRenderStateDirty();
if (AActor* OwnerActor = Comp->GetOwner())
{
OwnerActor->ForceNetUpdate();
}
}
else
{
Actor->ForceNetUpdate();
}
// Auto-call OnRep on the server (ImportText bypasses replication notify)
if (!Row.CachedProperty->RepNotifyFunc.IsNone())
{
if (UFunction* RepFunc = Target->FindFunction(Row.CachedProperty->RepNotifyFunc))
{
Target->ProcessEvent(RepFunc, nullptr);
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Called %s on %s"), *Row.CachedProperty->RepNotifyFunc.ToString(), *Target->GetName());
}
} }
// Export new value after import (normalized form for undo) // Export new value after import (normalized form for undo)
@@ -2184,6 +2297,14 @@ void UPS_Editor_MainWidget::OnModeButtonClicked(EPS_Editor_TransformMode Mode)
} }
} }
void UPS_Editor_MainWidget::ShowLoadingScreen(bool bShow)
{
if (LoadingOverlay.IsValid())
{
LoadingOverlay->SetVisibility(bShow ? EVisibility::Visible : EVisibility::Collapsed);
}
}
void UPS_Editor_MainWidget::OnCloseClicked() void UPS_Editor_MainWidget::OnCloseClicked()
{ {
if (CloseConfirmOverlay.IsValid()) if (CloseConfirmOverlay.IsValid())

View File

@@ -105,11 +105,21 @@ private:
TSharedPtr<SWidget> SplineEditButton; TSharedPtr<SWidget> SplineEditButton;
TSharedPtr<SWidget> SplineDeletePointButton; TSharedPtr<SWidget> SplineDeletePointButton;
// Simulate button
TSharedPtr<STextBlock> SimulateButtonText;
// Close confirmation // Close confirmation
TSharedPtr<SWidget> CloseConfirmOverlay; TSharedPtr<SWidget> CloseConfirmOverlay;
bool bSceneDirty = false; bool bSceneDirty = false;
int32 LastKnownSpawnedCount = 0; int32 LastKnownSpawnedCount = 0;
// Loading screen
TSharedPtr<SWidget> LoadingOverlay;
public:
/** Show/hide the loading overlay. */
void ShowLoadingScreen(bool bShow);
// ---- Helpers ---- // ---- Helpers ----
TSharedRef<SWidget> BuildToolbar(); TSharedRef<SWidget> BuildToolbar();
TSharedRef<SWidget> BuildPropertiesPanel(); TSharedRef<SWidget> BuildPropertiesPanel();