diff --git a/Unreal/Plugins/PS_BehaviorEditor/PS_BehaviorEditor.uplugin b/Unreal/Plugins/PS_BehaviorEditor/PS_BehaviorEditor.uplugin new file mode 100644 index 0000000..7aa0fb8 --- /dev/null +++ b/Unreal/Plugins/PS_BehaviorEditor/PS_BehaviorEditor.uplugin @@ -0,0 +1,30 @@ +{ + "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", + "Modules": [ + { + "Name": "PS_BehaviorEditor", + "Type": "Runtime", + "LoadingPhase": "Default" + } + ], + "Plugins": [ + { + "Name": "PS_Editor", + "Enabled": true + }, + { + "Name": "PS_AI_Behavior", + "Enabled": true + }, + { + "Name": "ProceduralMeshComponent", + "Enabled": true + } + ] +} diff --git a/Unreal/Plugins/PS_BehaviorEditor/Source/PS_BehaviorEditor/PS_BehaviorEditor.Build.cs b/Unreal/Plugins/PS_BehaviorEditor/Source/PS_BehaviorEditor/PS_BehaviorEditor.Build.cs new file mode 100644 index 0000000..36726b1 --- /dev/null +++ b/Unreal/Plugins/PS_BehaviorEditor/Source/PS_BehaviorEditor/PS_BehaviorEditor.Build.cs @@ -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" + }); + } +} diff --git a/Unreal/Plugins/PS_BehaviorEditor/Source/PS_BehaviorEditor/PS_BehaviorEditor.cpp b/Unreal/Plugins/PS_BehaviorEditor/Source/PS_BehaviorEditor/PS_BehaviorEditor.cpp new file mode 100644 index 0000000..128319e --- /dev/null +++ b/Unreal/Plugins/PS_BehaviorEditor/Source/PS_BehaviorEditor/PS_BehaviorEditor.cpp @@ -0,0 +1,3 @@ +#include "PS_BehaviorEditor.h" + +IMPLEMENT_MODULE(FPS_BehaviorEditorModule, PS_BehaviorEditor) diff --git a/Unreal/Plugins/PS_BehaviorEditor/Source/PS_BehaviorEditor/PS_BehaviorEditor.h b/Unreal/Plugins/PS_BehaviorEditor/Source/PS_BehaviorEditor/PS_BehaviorEditor.h new file mode 100644 index 0000000..47d0133 --- /dev/null +++ b/Unreal/Plugins/PS_BehaviorEditor/Source/PS_BehaviorEditor/PS_BehaviorEditor.h @@ -0,0 +1,10 @@ +#pragma once + +#include "Modules/ModuleManager.h" + +class FPS_BehaviorEditorModule : public IModuleInterface +{ +public: + virtual void StartupModule() override {} + virtual void ShutdownModule() override {} +}; diff --git a/Unreal/Plugins/PS_BehaviorEditor/Source/PS_BehaviorEditor/PS_BehaviorEditor_AISpline.cpp b/Unreal/Plugins/PS_BehaviorEditor/Source/PS_BehaviorEditor/PS_BehaviorEditor_AISpline.cpp new file mode 100644 index 0000000..ff6160b --- /dev/null +++ b/Unreal/Plugins/PS_BehaviorEditor/Source/PS_BehaviorEditor/PS_BehaviorEditor_AISpline.cpp @@ -0,0 +1,474 @@ +#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" + +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(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); + + // Handle parent + HandleRoot = CreateDefaultSubobject(TEXT("HandleRoot")); + HandleRoot->SetupAttachment(RootComponent); + + // Spawnable: makes this actor appear in the editor catalogue + SpawnableComp = CreateDefaultSubobject(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(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( + nullptr, TEXT("/PS_Editor/M_PS_Editor_Gizmo.M_PS_Editor_Gizmo")); + + // Opaque depth-tested material for spline line + UMaterialInterface* SplineLineMat = LoadObject( + nullptr, TEXT("/PS_Editor/M_PS_Editor_SplineLine.M_PS_Editor_SplineLine")); + if (!SplineLineMat) + { + SplineLineMat = LoadObject(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); + } + + // Center cube (selection handle) + CenterCube = NewObject(this); + CenterCube->SetupAttachment(GetRootComponent()); + CenterCube->RegisterComponent(); + UStaticMesh* CubeMesh = LoadObject(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); + } +} + +// ---- Point manipulation ---- + +void APS_BehaviorEditor_AISpline::AddPoint(const FVector& WorldPosition) +{ + SplineComp->AddSplinePoint(WorldPosition, ESplineCoordinateSpace::World, true); + PointHandles.Add(CreatePointHandle(WorldPosition)); + UpdateVisualization(); +} + +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(); +} + +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 APS_BehaviorEditor_AISpline::GetAllPointLocations() const +{ + TArray 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& Points) +{ + if (!SplineComp) return; + SplineComp->ClearSplinePoints(false); + for (const FVector& WorldPos : Points) + { + SplineComp->AddSplinePoint(WorldPos, ESplineCoordinateSpace::World, false); + } + SplineComp->UpdateSpline(); + RebuildHandles(); + UpdateVisualization(); +} + +void APS_BehaviorEditor_AISpline::InsertPoint(int32 Index, const FVector& WorldPosition) +{ + if (!SplineComp) return; + Index = FMath::Clamp(Index, 0, GetNumPoints()); + TArray Points = GetAllPointLocations(); + Points.Insert(WorldPosition, Index); + SetAllPointLocations(Points); +} + +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 Vertices; + TArray Triangles; + TArray Normals; + TArray 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(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(i) / (NumSamples - 1); + + for (int32 j = 0; j < Segs; j++) + { + const float Angle = (static_cast(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(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(), TArray(), 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(this); + Handle->SetupAttachment(HandleRoot); + Handle->RegisterComponent(); + UStaticMesh* SphereMesh = LoadObject(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); + } + 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); + } +} + +// ---- Serialization ---- + +void APS_BehaviorEditor_AISpline::ExportToCustomProperties(TMap& 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(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& Properties) +{ + // Spline points + const FString* CountStr = Properties.Find(TEXT("SplinePointCount")); + if (CountStr) + { + const int32 Num = FCString::Atoi(**CountStr); + if (Num >= 2) + { + TArray 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(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(); +} diff --git a/Unreal/Plugins/PS_BehaviorEditor/Source/PS_BehaviorEditor/PS_BehaviorEditor_AISpline.h b/Unreal/Plugins/PS_BehaviorEditor/Source/PS_BehaviorEditor/PS_BehaviorEditor_AISpline.h new file mode 100644 index 0000000..a3cc85b --- /dev/null +++ b/Unreal/Plugins/PS_BehaviorEditor/Source/PS_BehaviorEditor/PS_BehaviorEditor_AISpline.h @@ -0,0 +1,118 @@ +#pragma once + +#include "CoreMinimal.h" +#include "PS_AI_Behavior_SplinePath.h" +#include "PS_Editor_PointPlaceable.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 +{ + 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 ---- + + void AddPoint(const FVector& WorldPosition); + void MovePoint(int32 Index, const FVector& NewWorldPosition); + void RemovePoint(int32 Index); + int32 GetNumPoints() const; + FVector GetPointLocation(int32 Index) const; + TArray GetAllPointLocations() const; + void SetAllPointLocations(const TArray& Points); + void InsertPoint(int32 Index, const FVector& WorldPosition); + int32 FindInsertIndex(const FVector& WorldLocation) const; + FVector GetCentroid() const; + + // ---- Visualization ---- + + void UpdateVisualization(); + + UFUNCTION(BlueprintCallable, Category = "PS Editor|Spline") + void UpdateSplineMaterials(); + + // ---- Handle interaction ---- + + int32 GetHandleIndexUnderCursor(const FVector& RayOrigin, const FVector& RayDirection, float SphereRadius = 15.0f) const; + void SetHandlesVisible(bool bVisible); + void SetSelected(bool bSelected); + bool IsCenterCube(const UPrimitiveComponent* Comp) const; + bool IsSplineMesh(const UPrimitiveComponent* Comp) const; + void HighlightHandle(int32 Index); + void ClearHandleHighlight(); + + // ---- Serialization ---- + + void ExportToCustomProperties(TMap& OutProperties) const; + void ImportFromCustomProperties(const TMap& Properties); + +protected: + virtual void BeginPlay() override; + +private: + UPROPERTY(VisibleAnywhere) + TObjectPtr SplineMeshComp; + + UPROPERTY(VisibleAnywhere) + TObjectPtr HandleRoot; + + UPROPERTY() + TArray> PointHandles; + + UPROPERTY() + TObjectPtr SplineMat; + UPROPERTY() + TObjectPtr SplineMatSelected; + UPROPERTY() + TObjectPtr HandleMat; + UPROPERTY() + TObjectPtr HandleHighlightMat; + + UPROPERTY() + TObjectPtr CenterCube; + UPROPERTY() + TObjectPtr CenterCubeMat; + + UPROPERTY(VisibleAnywhere) + TObjectPtr SpawnableComp; + UPROPERTY(VisibleAnywhere) + TObjectPtr 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; +};