Files
UnrealEngine/Engine/Source/Runtime/NavigationSystem/Private/SplineNavModifierComponent.cpp
2025-05-18 13:04:45 +08:00

207 lines
7.5 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "SplineNavModifierComponent.h"
#include "AI/NavigationSystemBase.h"
#include "AI/Navigation/NavigationRelevantData.h"
#include "Components/SplineComponent.h"
#include "Curves/BezierUtilities.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(SplineNavModifierComponent)
namespace
{
// Subdivide the spline into linear segments, adapting to its curvature (more curvy means more linear segments)
void SubdivideSpline(TArray<FVector>& OutSubdivisions, const USplineComponent& Spline, const float SubdivisionThreshold)
{
// Sample at least 2 points
const int32 NumSplinePoints = FMath::Max(Spline.GetNumberOfSplinePoints(), 2);
// The USplineComponent's Hermite spline tangents are 3 times larger than Bezier tangents and we need to convert before tessellation
constexpr double HermiteToBezierFactor = 3.0;
// Tessellate the spline segments
int32 PrevIndex = Spline.IsClosedLoop() ? (NumSplinePoints - 1) : INDEX_NONE;
for (int32 SplinePointIndex = 0; SplinePointIndex < NumSplinePoints; SplinePointIndex++)
{
if (PrevIndex >= 0)
{
const FSplinePoint PrevSplinePoint = Spline.GetSplinePointAt(PrevIndex, ESplineCoordinateSpace::World);
const FSplinePoint CurrSplinePoint = Spline.GetSplinePointAt(SplinePointIndex, ESplineCoordinateSpace::World);
// The first point of the segment is appended before tessellation since UE::CubicBezier::Tessellate does not add it
OutSubdivisions.Add(PrevSplinePoint.Position);
// Convert this segment of the spline from Hermite to Bezier and subdivide it
UE::CubicBezier::Tessellate(OutSubdivisions,
PrevSplinePoint.Position,
PrevSplinePoint.Position + PrevSplinePoint.LeaveTangent / HermiteToBezierFactor,
CurrSplinePoint.Position - CurrSplinePoint.ArriveTangent / HermiteToBezierFactor,
CurrSplinePoint.Position,
SubdivisionThreshold);
}
PrevIndex = SplinePointIndex;
}
}
}
void USplineNavModifierComponent::CalculateBounds() const
{
Bounds = FBox(ForceInit);
if (const USplineComponent* Spline = Cast<USplineComponent>(AttachedSpline.GetComponent(GetOwner())))
{
// The largest stroke length is used to expand the bounds
const double Buffer = FMath::Max(StrokeWidth / 2.0, StrokeHeight / 2.0);
Bounds = Spline->CalcBounds(SplineTransform).GetBox().ExpandBy(Buffer);
}
}
void USplineNavModifierComponent::GetNavigationData(FNavigationRelevantData& Data) const
{
const USplineComponent* Spline = Cast<USplineComponent>(AttachedSpline.GetComponent(GetOwner()));
if (!Spline)
{
return;
}
// Build a rectangle in the YZ plane used to sample the spline at each cross section
constexpr int32 NumCrossSectionVertices = 4;
const double StrokeHalfWidth = StrokeWidth / 2.0;
const double StrokeHalfHeight = StrokeHeight / 2.0;
TStaticArray<FVector, NumCrossSectionVertices> CrossSectionRect;
CrossSectionRect[0] = FVector(0.0, -StrokeHalfWidth, -StrokeHalfHeight);
CrossSectionRect[1] = FVector(0.0, StrokeHalfWidth, -StrokeHalfHeight);
CrossSectionRect[2] = FVector(0.0, StrokeHalfWidth, StrokeHalfHeight);
CrossSectionRect[3] = FVector(0.0, -StrokeHalfWidth, StrokeHalfHeight);
// Vertices (in an arbitrary order) of a prism which will enclose each segment of the spline
TStaticArray<FVector, NumCrossSectionVertices * 2> Tube;
// Subdivide the spline so that high curvature sections get smaller and more linear segments than straighter sections
TArray<FVector> Subdivisions;
SubdivideSpline(Subdivisions, *Spline, GetSubdivisionThreshold());
const int32 NumSubdivisions = Subdivisions.Num();
// Create volumes from the spline subdivisions and use them to mark the nav mesh with the given are
const FTransform ComponentTransform = Spline->GetComponentTransform();
int32 PrevIndex = 0;
for (int32 SubdivisionIndex = 1; SubdivisionIndex < NumSubdivisions; SubdivisionIndex++)
{
// Compute the rotation of this tube segment
const double TubeAngle = (Subdivisions[SubdivisionIndex] - Subdivisions[PrevIndex]).HeadingAngle();
const FQuat TubeRotation(FVector::UnitZ(), TubeAngle);
// Compute the vertices of this tube segment
for (int i = 0; i < NumCrossSectionVertices; i++)
{
// For each vertex of the tube segment, first rotate about the positive Z axis, then translate to the subdivision point
Tube[i] = (TubeRotation * CrossSectionRect[i]) + Subdivisions[PrevIndex];
Tube[i + NumCrossSectionVertices] = (TubeRotation * CrossSectionRect[i]) + Subdivisions[SubdivisionIndex];
}
// From the tube construct a convex hull whose volume will be used to mark the nav mesh with the selected AreaClass
const FAreaNavModifier NavModifier(Tube, ENavigationCoordSystem::Type::Unreal, ComponentTransform, AreaClass);
Data.Modifiers.Add(NavModifier);
PrevIndex = SubdivisionIndex;
}
}
USplineNavModifierComponent::USplineNavModifierComponent(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{
#if WITH_EDITORONLY_DATA
// Should tick in the editor in order to track whether the spline has updated
bTickInEditor = true;
PrimaryComponentTick.bCanEverTick = true;
// If a spline is already attached, store its update-checking data
if (const USplineComponent* Spline = Cast<USplineComponent>(AttachedSpline.GetComponent(GetOwner())))
{
SplineVersion = Spline->GetVersion();
SplineTransform = Spline->GetComponentTransform();
}
#endif // WITH_EDITORONLY_DATA
}
void USplineNavModifierComponent::UpdateNavigationWithComponentData()
{
#if WITH_EDITORONLY_DATA
CalculateBounds();
FNavigationSystem::UpdateComponentData(*this);
#endif // WITH_EDITORONLY_DATA
}
#if WITH_EDITORONLY_DATA
bool USplineNavModifierComponent::IsComponentTickEnabled() const
{
const UWorld* World = GetWorld();
return World && !World->IsGameWorld();
}
void USplineNavModifierComponent::TickComponent(const float DeltaTime, const ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
if (const USplineComponent* Spline = Cast<USplineComponent>(AttachedSpline.GetComponent(GetOwner())))
{
// Update spline data, and if anything changed then update nav data
if (SplineVersion != INVALID_SPLINE_VERSION)
{
bool bRequiresNavigationUpdate = false;
const uint32 NextVersion = Spline->GetVersion();
if (SplineVersion != NextVersion)
{
SplineVersion = NextVersion;
bRequiresNavigationUpdate = true;
}
const FTransform& NextTransform = Spline->GetComponentTransform();
if (!SplineTransform.Equals(NextTransform))
{
SplineTransform = NextTransform;
bRequiresNavigationUpdate = true;
}
// This can be expensive (i.e. updating every tick as the user drags a spline point), so only update nav data if the editor flag is set
if (bRequiresNavigationUpdate && bUpdateNavDataOnSplineChange)
{
UpdateNavigationWithComponentData();
}
}
else
{
// The spline just became valid; store its data and use it to update nav data
SplineVersion = Spline->GetVersion();
SplineTransform = Spline->GetComponentTransform();
UpdateNavigationWithComponentData();
}
}
else if (SplineVersion != INVALID_SPLINE_VERSION)
{
// The spline just became invalid; reset the version and recompute nav data without the spline
SplineVersion = INVALID_SPLINE_VERSION;
UpdateNavigationWithComponentData();
}
}
#endif // WITH_EDITORONLY_DATA
float USplineNavModifierComponent::GetSubdivisionThreshold() const
{
switch (SubdivisionLOD)
{
case ESubdivisionLOD::Ultra:
return 10.0f;
case ESubdivisionLOD::High:
return 100.0f;
case ESubdivisionLOD::Medium:
return 250.0f;
case ESubdivisionLOD::Low:
default: // Fallthrough
return 500.0f;
}
}