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>
This commit is contained in:
30
Unreal/Plugins/PS_BehaviorEditor/PS_BehaviorEditor.uplugin
Normal file
30
Unreal/Plugins/PS_BehaviorEditor/PS_BehaviorEditor.uplugin
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
#include "PS_BehaviorEditor.h"
|
||||||
|
|
||||||
|
IMPLEMENT_MODULE(FPS_BehaviorEditorModule, PS_BehaviorEditor)
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Modules/ModuleManager.h"
|
||||||
|
|
||||||
|
class FPS_BehaviorEditorModule : public IModuleInterface
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual void StartupModule() override {}
|
||||||
|
virtual void ShutdownModule() override {}
|
||||||
|
};
|
||||||
@@ -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<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);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 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<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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
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<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();
|
||||||
|
}
|
||||||
@@ -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<FVector> GetAllPointLocations() const;
|
||||||
|
void SetAllPointLocations(const TArray<FVector>& 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<FString, FString>& OutProperties) const;
|
||||||
|
void ImportFromCustomProperties(const TMap<FString, FString>& Properties);
|
||||||
|
|
||||||
|
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<UStaticMeshComponent> CenterCube;
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UMaterialInstanceDynamic> CenterCubeMat;
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user