Fix perception, patrol, and spline following for PS_AI_Behavior
Perception: - Configure senses in BeginPlay + RequestStimuliListenerUpdate (not constructor) - Filter threats by team attitude: only Hostile actors generate threat - Skip Friendly and Neutral actors in CalculateThreatLevel and GetHighestThreatActor - All Hostile actors are valid threats regardless of TargetPriority list Blackboard: - Use SetValueAsEnum/GetValueAsEnum for BehaviorState key (was Int) Patrol: - Auto NavMesh patrol when no manual waypoints defined - Project HomeLocation onto NavMesh before searching random points - Fallback to NPC current position if HomeLocation not on NavMesh Spline following: - Choose direction based on NPC forward vector vs spline tangent (not longest distance) - Skip re-search if already following a spline (prevent BT re-boucle) - Reverse at end: wait for NPC to catch up before inverting direction - Use NPC actual CharacterMovement speed for target point advancement - Face toward target point (not spline tangent) to prevent crab-walking in turns - Junction switching: direction continuity check, 70% switch chance, world gap tolerance - Debug draw: green sphere (target), yellow line (gap), cyan arrow (tangent), text overlay - Add GetWorldDirectionAtDistance to SplinePath Editor: - Add Placeable to SplinePath and CoverPoint UCLASS - Add PersonalityProfileFactory for Data Asset creation - Add EnsureBlackboardAsset declaration Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
909583a1bb
commit
2e04cb0334
65
Build_Plugin.bat
Normal file
65
Build_Plugin.bat
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
|
||||||
|
:: ─── Configuration ──────────────────────────────────────────────────────────
|
||||||
|
set UE_ROOT=C:\Program Files\Epic Games\UE_5.5
|
||||||
|
set UPROJECT=E:\ASTERION\GIT\PS_AI_Agent\Unreal\PS_AI_Agent\PS_AI_Agent.uproject
|
||||||
|
set PLATFORM=Win64
|
||||||
|
set CONFIG=Development
|
||||||
|
set TARGET=PS_AI_AgentEditor
|
||||||
|
|
||||||
|
:: ─── Colors ─────────────────────────────────────────────────────────────────
|
||||||
|
set GREEN=[92m
|
||||||
|
set RED=[91m
|
||||||
|
set YELLOW=[93m
|
||||||
|
set RESET=[0m
|
||||||
|
|
||||||
|
:: ─── Banner ─────────────────────────────────────────────────────────────────
|
||||||
|
echo.
|
||||||
|
echo %GREEN%============================================%RESET%
|
||||||
|
echo PS_AI_Agent - Plugin Build
|
||||||
|
echo Target: %TARGET% %PLATFORM% %CONFIG%
|
||||||
|
echo %GREEN%============================================%RESET%
|
||||||
|
echo.
|
||||||
|
|
||||||
|
:: ─── Verify paths ───────────────────────────────────────────────────────────
|
||||||
|
if not exist "%UE_ROOT%\Engine\Binaries\ThirdParty\DotNet\8.0.300\win-x64\dotnet.exe" (
|
||||||
|
echo %RED%ERROR: UE5.5 dotnet not found at %UE_ROOT%%RESET%
|
||||||
|
echo Check UE_ROOT path in this script.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
if not exist "%UPROJECT%" (
|
||||||
|
echo %RED%ERROR: .uproject not found: %UPROJECT%%RESET%
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
:: ─── Build ──────────────────────────────────────────────────────────────────
|
||||||
|
echo %YELLOW%Building %TARGET% (%CONFIG%)...%RESET%
|
||||||
|
echo.
|
||||||
|
|
||||||
|
"%UE_ROOT%\Engine\Binaries\ThirdParty\DotNet\8.0.300\win-x64\dotnet.exe" ^
|
||||||
|
"%UE_ROOT%\Engine\Binaries\DotNET\UnrealBuildTool\UnrealBuildTool.dll" ^
|
||||||
|
%TARGET% %PLATFORM% %CONFIG% ^
|
||||||
|
-Project="%UPROJECT%" ^
|
||||||
|
-WaitMutex ^
|
||||||
|
-FromMsBuild
|
||||||
|
|
||||||
|
set BUILD_EXIT=%ERRORLEVEL%
|
||||||
|
|
||||||
|
echo.
|
||||||
|
if %BUILD_EXIT% EQU 0 (
|
||||||
|
echo %GREEN%============================================%RESET%
|
||||||
|
echo BUILD SUCCEEDED
|
||||||
|
echo %GREEN%============================================%RESET%
|
||||||
|
) else (
|
||||||
|
echo %RED%============================================%RESET%
|
||||||
|
echo BUILD FAILED (exit code: %BUILD_EXIT%)
|
||||||
|
echo %RED%============================================%RESET%
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
|
exit /b %BUILD_EXIT%
|
||||||
Binary file not shown.
@ -27,7 +27,30 @@ void UPS_AI_Behavior_BTService_UpdateThreat::TickNode(
|
|||||||
if (!BB) return;
|
if (!BB) return;
|
||||||
|
|
||||||
UPS_AI_Behavior_PerceptionComponent* Perception = AIC->GetBehaviorPerception();
|
UPS_AI_Behavior_PerceptionComponent* Perception = AIC->GetBehaviorPerception();
|
||||||
if (!Perception) return;
|
if (!Perception)
|
||||||
|
{
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Warning, TEXT("[%s] UpdateThreat: No PerceptionComponent!"), *AIC->GetName());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug: check what perception sees
|
||||||
|
TArray<AActor*> PerceivedActors;
|
||||||
|
Perception->GetCurrentlyPerceivedActors(nullptr, PerceivedActors);
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[%s] UpdateThreat: Perceived %d actors"),
|
||||||
|
*AIC->GetName(), PerceivedActors.Num());
|
||||||
|
for (AActor* A : PerceivedActors)
|
||||||
|
{
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Verbose, TEXT(" - %s"), *A->GetName());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also check known actors (perceived in the past but maybe lost sight)
|
||||||
|
TArray<AActor*> KnownActors;
|
||||||
|
Perception->GetKnownPerceivedActors(nullptr, KnownActors);
|
||||||
|
if (KnownActors.Num() != PerceivedActors.Num())
|
||||||
|
{
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[%s] UpdateThreat: Known (past) %d actors"),
|
||||||
|
*AIC->GetName(), KnownActors.Num());
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate current threat
|
// Calculate current threat
|
||||||
const float RawThreat = Perception->CalculateThreatLevel();
|
const float RawThreat = Perception->CalculateThreatLevel();
|
||||||
|
|||||||
@ -31,6 +31,13 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_FindAndFollowSpline::ExecuteTask(
|
|||||||
return EBTNodeResult::Failed;
|
return EBTNodeResult::Failed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If already following a spline, don't re-search — just succeed immediately
|
||||||
|
// The Follow Spline task will continue the movement
|
||||||
|
if (Follower->bIsFollowing && Follower->CurrentSpline)
|
||||||
|
{
|
||||||
|
return EBTNodeResult::Succeeded;
|
||||||
|
}
|
||||||
|
|
||||||
// Determine NPC type
|
// Determine NPC type
|
||||||
EPS_AI_Behavior_NPCType NPCType = EPS_AI_Behavior_NPCType::Civilian;
|
EPS_AI_Behavior_NPCType NPCType = EPS_AI_Behavior_NPCType::Civilian;
|
||||||
UPS_AI_Behavior_PersonalityComponent* Personality = AIC->GetPersonalityComponent();
|
UPS_AI_Behavior_PersonalityComponent* Personality = AIC->GetPersonalityComponent();
|
||||||
@ -66,6 +73,11 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_FindAndFollowSpline::ExecuteTask(
|
|||||||
const FVector SplinePoint = ClosestSpline->GetWorldLocationAtDistance(DistAlongSpline);
|
const FVector SplinePoint = ClosestSpline->GetWorldLocationAtDistance(DistAlongSpline);
|
||||||
const float GapToSpline = FVector::Dist(AIC->GetPawn()->GetActorLocation(), SplinePoint);
|
const float GapToSpline = FVector::Dist(AIC->GetPawn()->GetActorLocation(), SplinePoint);
|
||||||
|
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Verbose,
|
||||||
|
TEXT("[%s] FindAndFollowSpline: found spline '%s' at dist=%.0f/%.0f, gap=%.0fcm"),
|
||||||
|
*AIC->GetName(), *ClosestSpline->GetName(), DistAlongSpline,
|
||||||
|
ClosestSpline->GetSplineLength(), GapToSpline);
|
||||||
|
|
||||||
if (bWalkToSpline && GapToSpline > AcceptanceRadius)
|
if (bWalkToSpline && GapToSpline > AcceptanceRadius)
|
||||||
{
|
{
|
||||||
// Walk to spline first via NavMesh
|
// Walk to spline first via NavMesh
|
||||||
@ -77,13 +89,19 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_FindAndFollowSpline::ExecuteTask(
|
|||||||
if (Result == EPathFollowingRequestResult::Failed)
|
if (Result == EPathFollowingRequestResult::Failed)
|
||||||
{
|
{
|
||||||
// Can't reach via NavMesh — try starting anyway (snap)
|
// Can't reach via NavMesh — try starting anyway (snap)
|
||||||
Follower->StartFollowingAtDistance(ClosestSpline, DistAlongSpline);
|
const FVector Fwd = AIC->GetPawn()->GetActorForwardVector();
|
||||||
|
const FVector SpDir = ClosestSpline->GetWorldDirectionAtDistance(DistAlongSpline);
|
||||||
|
Follower->StartFollowingAtDistance(ClosestSpline, DistAlongSpline,
|
||||||
|
FVector::DotProduct(Fwd, SpDir) >= 0.0f);
|
||||||
return EBTNodeResult::Succeeded;
|
return EBTNodeResult::Succeeded;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Result == EPathFollowingRequestResult::AlreadyAtGoal)
|
if (Result == EPathFollowingRequestResult::AlreadyAtGoal)
|
||||||
{
|
{
|
||||||
Follower->StartFollowingAtDistance(ClosestSpline, DistAlongSpline);
|
const FVector Fwd = AIC->GetPawn()->GetActorForwardVector();
|
||||||
|
const FVector SpDir = ClosestSpline->GetWorldDirectionAtDistance(DistAlongSpline);
|
||||||
|
Follower->StartFollowingAtDistance(ClosestSpline, DistAlongSpline,
|
||||||
|
FVector::DotProduct(Fwd, SpDir) >= 0.0f);
|
||||||
return EBTNodeResult::Succeeded;
|
return EBTNodeResult::Succeeded;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,7 +115,26 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_FindAndFollowSpline::ExecuteTask(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Close enough — start immediately
|
// Close enough — start immediately
|
||||||
Follower->StartFollowingAtDistance(ClosestSpline, DistAlongSpline);
|
// Choose direction based on NPC's current movement direction vs spline tangent
|
||||||
|
const FVector NpcForward = AIC->GetPawn()->GetActorForwardVector();
|
||||||
|
const FVector SplineDirForward = ClosestSpline->GetWorldDirectionAtDistance(DistAlongSpline);
|
||||||
|
const float DotForward = FVector::DotProduct(NpcForward, SplineDirForward);
|
||||||
|
const bool bForward = (DotForward >= 0.0f);
|
||||||
|
|
||||||
|
// If NPC is very close to an end, ensure we don't start going into a wall
|
||||||
|
const float SplineLen = ClosestSpline->GetSplineLength();
|
||||||
|
const float MinEndMargin = 50.0f;
|
||||||
|
bool bFinalForward = bForward;
|
||||||
|
if (bFinalForward && DistAlongSpline > SplineLen - MinEndMargin)
|
||||||
|
{
|
||||||
|
bFinalForward = false; // Too close to the end, go backward
|
||||||
|
}
|
||||||
|
else if (!bFinalForward && DistAlongSpline < MinEndMargin)
|
||||||
|
{
|
||||||
|
bFinalForward = true; // Too close to the start, go forward
|
||||||
|
}
|
||||||
|
|
||||||
|
Follower->StartFollowingAtDistance(ClosestSpline, DistAlongSpline, bFinalForward);
|
||||||
return EBTNodeResult::Succeeded;
|
return EBTNodeResult::Succeeded;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
#include "PS_AI_Behavior_AIController.h"
|
#include "PS_AI_Behavior_AIController.h"
|
||||||
#include "PS_AI_Behavior_Definitions.h"
|
#include "PS_AI_Behavior_Definitions.h"
|
||||||
#include "BehaviorTree/BlackboardComponent.h"
|
#include "BehaviorTree/BlackboardComponent.h"
|
||||||
|
#include "NavigationSystem.h"
|
||||||
#include "Navigation/PathFollowingComponent.h"
|
#include "Navigation/PathFollowingComponent.h"
|
||||||
|
|
||||||
UPS_AI_Behavior_BTTask_Patrol::UPS_AI_Behavior_BTTask_Patrol()
|
UPS_AI_Behavior_BTTask_Patrol::UPS_AI_Behavior_BTTask_Patrol()
|
||||||
@ -17,27 +18,39 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_Patrol::ExecuteTask(
|
|||||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
|
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
|
||||||
{
|
{
|
||||||
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp.GetAIOwner());
|
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp.GetAIOwner());
|
||||||
if (!AIC) return EBTNodeResult::Failed;
|
if (!AIC || !AIC->GetPawn()) return EBTNodeResult::Failed;
|
||||||
|
|
||||||
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
|
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
|
||||||
if (!BB) return EBTNodeResult::Failed;
|
if (!BB) return EBTNodeResult::Failed;
|
||||||
|
|
||||||
// Check we have patrol points
|
|
||||||
if (AIC->PatrolPoints.Num() == 0)
|
|
||||||
{
|
|
||||||
UE_LOG(LogPS_AI_Behavior, Warning, TEXT("[%s] Patrol task: no patrol points defined."), *AIC->GetName());
|
|
||||||
return EBTNodeResult::Failed;
|
|
||||||
}
|
|
||||||
|
|
||||||
FPatrolMemory* Memory = reinterpret_cast<FPatrolMemory*>(NodeMemory);
|
FPatrolMemory* Memory = reinterpret_cast<FPatrolMemory*>(NodeMemory);
|
||||||
Memory->bIsWaiting = false;
|
Memory->bIsWaiting = false;
|
||||||
Memory->bMoveRequested = false;
|
Memory->bMoveRequested = false;
|
||||||
Memory->WaitRemaining = 0.0f;
|
Memory->WaitRemaining = 0.0f;
|
||||||
|
|
||||||
// Get current patrol index
|
FVector Destination;
|
||||||
const int32 PatrolIdx = BB->GetValueAsInt(PS_AI_Behavior_BB::PatrolIndex);
|
|
||||||
const int32 SafeIdx = PatrolIdx % AIC->PatrolPoints.Num();
|
if (AIC->PatrolPoints.Num() > 0)
|
||||||
const FVector Destination = AIC->PatrolPoints[SafeIdx];
|
{
|
||||||
|
// ─── Mode manuel : waypoints définis ────────────────────────────
|
||||||
|
const int32 PatrolIdx = BB->GetValueAsInt(PS_AI_Behavior_BB::PatrolIndex);
|
||||||
|
const int32 SafeIdx = PatrolIdx % AIC->PatrolPoints.Num();
|
||||||
|
Destination = AIC->PatrolPoints[SafeIdx];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// ─── Mode auto : point aléatoire sur NavMesh ────────────────────
|
||||||
|
const FVector HomeLoc = BB->GetValueAsVector(PS_AI_Behavior_BB::HomeLocation);
|
||||||
|
const FVector CurrentLoc = AIC->GetPawn()->GetActorLocation();
|
||||||
|
|
||||||
|
if (!FindRandomPatrolPoint(GetWorld(), HomeLoc, CurrentLoc, Destination))
|
||||||
|
{
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Verbose,
|
||||||
|
TEXT("[%s] Patrol: no valid NavMesh point found within %.0fcm."),
|
||||||
|
*AIC->GetName(), PatrolRadius);
|
||||||
|
return EBTNodeResult::Failed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Issue move request
|
// Issue move request
|
||||||
const EPathFollowingRequestResult::Type Result = AIC->MoveToLocation(
|
const EPathFollowingRequestResult::Type Result = AIC->MoveToLocation(
|
||||||
@ -47,19 +60,22 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_Patrol::ExecuteTask(
|
|||||||
|
|
||||||
if (Result == EPathFollowingRequestResult::Failed)
|
if (Result == EPathFollowingRequestResult::Failed)
|
||||||
{
|
{
|
||||||
UE_LOG(LogPS_AI_Behavior, Warning, TEXT("[%s] Patrol: MoveTo failed for point %d."),
|
UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[%s] Patrol: MoveTo failed."), *AIC->GetName());
|
||||||
*AIC->GetName(), SafeIdx);
|
|
||||||
return EBTNodeResult::Failed;
|
return EBTNodeResult::Failed;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Result == EPathFollowingRequestResult::AlreadyAtGoal)
|
if (Result == EPathFollowingRequestResult::AlreadyAtGoal)
|
||||||
{
|
{
|
||||||
// Already there — start wait
|
|
||||||
Memory->bIsWaiting = true;
|
Memory->bIsWaiting = true;
|
||||||
Memory->WaitRemaining = FMath::RandRange(MinWaitTime, MaxWaitTime);
|
Memory->WaitRemaining = FMath::RandRange(MinWaitTime, MaxWaitTime);
|
||||||
|
|
||||||
// Advance patrol index
|
// Advance patrol index (for manual mode)
|
||||||
BB->SetValueAsInt(PS_AI_Behavior_BB::PatrolIndex, (SafeIdx + 1) % AIC->PatrolPoints.Num());
|
if (AIC->PatrolPoints.Num() > 0)
|
||||||
|
{
|
||||||
|
const int32 PatrolIdx = BB->GetValueAsInt(PS_AI_Behavior_BB::PatrolIndex);
|
||||||
|
BB->SetValueAsInt(PS_AI_Behavior_BB::PatrolIndex,
|
||||||
|
(PatrolIdx + 1) % AIC->PatrolPoints.Num());
|
||||||
|
}
|
||||||
|
|
||||||
return EBTNodeResult::InProgress;
|
return EBTNodeResult::InProgress;
|
||||||
}
|
}
|
||||||
@ -88,24 +104,22 @@ void UPS_AI_Behavior_BTTask_Patrol::TickTask(
|
|||||||
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp.GetAIOwner());
|
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp.GetAIOwner());
|
||||||
if (!AIC) { FinishLatentTask(OwnerComp, EBTNodeResult::Failed); return; }
|
if (!AIC) { FinishLatentTask(OwnerComp, EBTNodeResult::Failed); return; }
|
||||||
|
|
||||||
const EPathFollowingStatus::Type MoveStatus = AIC->GetMoveStatus();
|
if (AIC->GetMoveStatus() == EPathFollowingStatus::Idle)
|
||||||
|
|
||||||
if (MoveStatus == EPathFollowingStatus::Idle)
|
|
||||||
{
|
{
|
||||||
// Move completed — start wait at waypoint
|
// Move completed — start wait
|
||||||
Memory->bMoveRequested = false;
|
Memory->bMoveRequested = false;
|
||||||
Memory->bIsWaiting = true;
|
Memory->bIsWaiting = true;
|
||||||
Memory->WaitRemaining = FMath::RandRange(MinWaitTime, MaxWaitTime);
|
Memory->WaitRemaining = FMath::RandRange(MinWaitTime, MaxWaitTime);
|
||||||
|
|
||||||
// Advance patrol index
|
// Advance patrol index (for manual mode)
|
||||||
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
|
if (AIC->PatrolPoints.Num() > 0)
|
||||||
if (BB)
|
|
||||||
{
|
{
|
||||||
const int32 PatrolIdx = BB->GetValueAsInt(PS_AI_Behavior_BB::PatrolIndex);
|
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
|
||||||
const int32 NumPoints = AIC->PatrolPoints.Num();
|
if (BB)
|
||||||
if (NumPoints > 0)
|
|
||||||
{
|
{
|
||||||
BB->SetValueAsInt(PS_AI_Behavior_BB::PatrolIndex, (PatrolIdx + 1) % NumPoints);
|
const int32 PatrolIdx = BB->GetValueAsInt(PS_AI_Behavior_BB::PatrolIndex);
|
||||||
|
BB->SetValueAsInt(PS_AI_Behavior_BB::PatrolIndex,
|
||||||
|
(PatrolIdx + 1) % AIC->PatrolPoints.Num());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -123,8 +137,58 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_Patrol::AbortTask(
|
|||||||
return EBTNodeResult::Aborted;
|
return EBTNodeResult::Aborted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool UPS_AI_Behavior_BTTask_Patrol::FindRandomPatrolPoint(
|
||||||
|
const UWorld* World, const FVector& HomeLoc,
|
||||||
|
const FVector& CurrentLoc, FVector& OutPoint) const
|
||||||
|
{
|
||||||
|
UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(
|
||||||
|
const_cast<UWorld*>(World));
|
||||||
|
if (!NavSys) return false;
|
||||||
|
|
||||||
|
// Project HomeLoc onto NavMesh first (spawn position might not be exactly on it)
|
||||||
|
FNavLocation ProjectedHome;
|
||||||
|
const FVector ProjectionExtent(500.0f, 500.0f, 500.0f);
|
||||||
|
FVector SearchOrigin = HomeLoc;
|
||||||
|
|
||||||
|
if (NavSys->ProjectPointToNavigation(HomeLoc, ProjectedHome, ProjectionExtent))
|
||||||
|
{
|
||||||
|
SearchOrigin = ProjectedHome.Location;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// HomeLoc not on NavMesh — try from current position instead
|
||||||
|
FNavLocation ProjectedCurrent;
|
||||||
|
if (NavSys->ProjectPointToNavigation(CurrentLoc, ProjectedCurrent, ProjectionExtent))
|
||||||
|
{
|
||||||
|
SearchOrigin = ProjectedCurrent.Location;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return false; // Neither home nor current pos are on NavMesh
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try multiple times to find a valid point far enough from current position
|
||||||
|
for (int32 Attempt = 0; Attempt < 10; ++Attempt)
|
||||||
|
{
|
||||||
|
FNavLocation NavLoc;
|
||||||
|
if (NavSys->GetRandomReachablePointInRadius(SearchOrigin, PatrolRadius, NavLoc))
|
||||||
|
{
|
||||||
|
const float DistFromCurrent = FVector::Dist2D(NavLoc.Location, CurrentLoc);
|
||||||
|
if (DistFromCurrent >= MinPatrolDistance)
|
||||||
|
{
|
||||||
|
OutPoint = NavLoc.Location;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
FString UPS_AI_Behavior_BTTask_Patrol::GetStaticDescription() const
|
FString UPS_AI_Behavior_BTTask_Patrol::GetStaticDescription() const
|
||||||
{
|
{
|
||||||
return FString::Printf(TEXT("Patrol (wait %.1f-%.1fs, radius %.0fcm)"),
|
return FString::Printf(
|
||||||
MinWaitTime, MaxWaitTime, AcceptanceRadius);
|
TEXT("Patrol (wait %.1f-%.1fs)\nManual waypoints OR auto NavMesh (radius %.0fcm, min dist %.0fcm)"),
|
||||||
|
MinWaitTime, MaxWaitTime, PatrolRadius, MinPatrolDistance);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,7 +16,8 @@
|
|||||||
|
|
||||||
UPS_AI_Behavior_PerceptionComponent::UPS_AI_Behavior_PerceptionComponent()
|
UPS_AI_Behavior_PerceptionComponent::UPS_AI_Behavior_PerceptionComponent()
|
||||||
{
|
{
|
||||||
// Senses are configured in BeginPlay after settings are available
|
// Senses must be configured after construction (NewObject not allowed in CDO constructors).
|
||||||
|
// We configure in BeginPlay and force a perception system update.
|
||||||
}
|
}
|
||||||
|
|
||||||
void UPS_AI_Behavior_PerceptionComponent::BeginPlay()
|
void UPS_AI_Behavior_PerceptionComponent::BeginPlay()
|
||||||
@ -24,6 +25,9 @@ void UPS_AI_Behavior_PerceptionComponent::BeginPlay()
|
|||||||
ConfigureSenses();
|
ConfigureSenses();
|
||||||
Super::BeginPlay();
|
Super::BeginPlay();
|
||||||
|
|
||||||
|
// Force the perception system to re-register our senses now that they're configured
|
||||||
|
RequestStimuliListenerUpdate();
|
||||||
|
|
||||||
OnPerceptionUpdated.AddDynamic(this, &UPS_AI_Behavior_PerceptionComponent::HandlePerceptionUpdated);
|
OnPerceptionUpdated.AddDynamic(this, &UPS_AI_Behavior_PerceptionComponent::HandlePerceptionUpdated);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,23 +188,35 @@ AActor* UPS_AI_Behavior_PerceptionComponent::GetHighestThreatActor(
|
|||||||
const AAIController* AIC = Cast<AAIController>(Owner);
|
const AAIController* AIC = Cast<AAIController>(Owner);
|
||||||
if (AIC && Actor == AIC->GetPawn()) continue;
|
if (AIC && Actor == AIC->GetPawn()) continue;
|
||||||
|
|
||||||
|
// Skip non-hostile actors (only Hostile actors are valid threats)
|
||||||
|
if (AIC)
|
||||||
|
{
|
||||||
|
const ETeamAttitude::Type Attitude = AIC->GetTeamAttitudeTowards(*Actor);
|
||||||
|
if (Attitude != ETeamAttitude::Hostile)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Classify this actor ────────────────────────────────────────
|
// ─── Classify this actor ────────────────────────────────────────
|
||||||
const EPS_AI_Behavior_TargetType ActorType = ClassifyActor(Actor);
|
const EPS_AI_Behavior_TargetType ActorType = ClassifyActor(Actor);
|
||||||
|
|
||||||
// Check if this target type is in our priority list at all
|
|
||||||
const int32 PriorityIndex = ActivePriority.IndexOfByKey(ActorType);
|
|
||||||
if (PriorityIndex == INDEX_NONE)
|
|
||||||
{
|
|
||||||
// Not a valid target for this NPC
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Score calculation ──────────────────────────────────────────
|
// ─── Score calculation ──────────────────────────────────────────
|
||||||
float Score = 0.0f;
|
float Score = 0.0f;
|
||||||
|
|
||||||
// Priority rank bonus: higher priority = much higher score
|
// Priority rank bonus: actors in the priority list score higher
|
||||||
// Max priority entries = ~4, so (4 - index) * 100 gives clear separation
|
// This is used for COMBAT targeting (who to attack first)
|
||||||
Score += (ActivePriority.Num() - PriorityIndex) * 100.0f;
|
// But ALL hostile actors are valid threats (for fleeing, alerting, etc.)
|
||||||
|
const int32 PriorityIndex = ActivePriority.IndexOfByKey(ActorType);
|
||||||
|
if (PriorityIndex != INDEX_NONE)
|
||||||
|
{
|
||||||
|
Score += (ActivePriority.Num() - PriorityIndex) * 100.0f;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Not in priority list but still Hostile — valid threat, lower score
|
||||||
|
Score += 10.0f;
|
||||||
|
}
|
||||||
|
|
||||||
// Damage sense override: actor that hit us gets a massive bonus
|
// Damage sense override: actor that hit us gets a massive bonus
|
||||||
// (bypasses priority — self-defense)
|
// (bypasses priority — self-defense)
|
||||||
@ -252,10 +268,23 @@ float UPS_AI_Behavior_PerceptionComponent::CalculateThreatLevel()
|
|||||||
TArray<AActor*> PerceivedActors;
|
TArray<AActor*> PerceivedActors;
|
||||||
GetCurrentlyPerceivedActors(nullptr, PerceivedActors); // All senses
|
GetCurrentlyPerceivedActors(nullptr, PerceivedActors); // All senses
|
||||||
|
|
||||||
|
// Get our AIController for attitude checks
|
||||||
|
const AAIController* AIC = Cast<AAIController>(Owner);
|
||||||
|
|
||||||
for (AActor* Actor : PerceivedActors)
|
for (AActor* Actor : PerceivedActors)
|
||||||
{
|
{
|
||||||
if (!Actor) continue;
|
if (!Actor) continue;
|
||||||
|
|
||||||
|
// Only count Hostile actors as threats (skip Friendly and Neutral)
|
||||||
|
if (AIC)
|
||||||
|
{
|
||||||
|
const ETeamAttitude::Type Attitude = AIC->GetTeamAttitudeTowards(*Actor);
|
||||||
|
if (Attitude != ETeamAttitude::Hostile)
|
||||||
|
{
|
||||||
|
continue; // Only Hostile actors generate threat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
float ActorThreat = 0.0f;
|
float ActorThreat = 0.0f;
|
||||||
const float Dist = FVector::Dist(OwnerLoc, Actor->GetActorLocation());
|
const float Dist = FVector::Dist(OwnerLoc, Actor->GetActorLocation());
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
#include "GameFramework/Character.h"
|
#include "GameFramework/Character.h"
|
||||||
#include "GameFramework/CharacterMovementComponent.h"
|
#include "GameFramework/CharacterMovementComponent.h"
|
||||||
#include "Net/UnrealNetwork.h"
|
#include "Net/UnrealNetwork.h"
|
||||||
|
#include "DrawDebugHelpers.h"
|
||||||
|
|
||||||
UPS_AI_Behavior_SplineFollowerComponent::UPS_AI_Behavior_SplineFollowerComponent()
|
UPS_AI_Behavior_SplineFollowerComponent::UPS_AI_Behavior_SplineFollowerComponent()
|
||||||
{
|
{
|
||||||
@ -59,6 +60,13 @@ bool UPS_AI_Behavior_SplineFollowerComponent::StartFollowingAtDistance(
|
|||||||
bIsFollowing = true;
|
bIsFollowing = true;
|
||||||
LastHandledJunctionIndex = -1;
|
LastHandledJunctionIndex = -1;
|
||||||
|
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Warning,
|
||||||
|
TEXT("[%s] StartFollowingAtDistance: spline='%s' dist=%.0f/%.0f dir=%s (was spline='%s')"),
|
||||||
|
GetOwner() ? *GetOwner()->GetName() : TEXT("?"),
|
||||||
|
*Spline->GetName(), CurrentDistance, Spline->GetSplineLength(),
|
||||||
|
bForward ? TEXT("FWD") : TEXT("BWD"),
|
||||||
|
OldSpline ? *OldSpline->GetName() : TEXT("none"));
|
||||||
|
|
||||||
if (OldSpline && OldSpline != Spline)
|
if (OldSpline && OldSpline != Spline)
|
||||||
{
|
{
|
||||||
OnSplineChanged.Broadcast(OldSpline, Spline);
|
OnSplineChanged.Broadcast(OldSpline, Spline);
|
||||||
@ -147,8 +155,17 @@ void UPS_AI_Behavior_SplineFollowerComponent::TickComponent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ─── Advance along spline ───────────────────────────────────────────
|
// ─── Advance along spline ───────────────────────────────────────────
|
||||||
const float Speed = GetEffectiveSpeed();
|
// Use the NPC's actual movement speed (from CMC) instead of a fixed speed.
|
||||||
|
// This keeps the target point in sync with how fast the character really moves.
|
||||||
|
float Speed = GetEffectiveSpeed();
|
||||||
|
ACharacter* OwnerCharacter = Cast<ACharacter>(GetOwner());
|
||||||
|
if (OwnerCharacter && OwnerCharacter->GetCharacterMovement())
|
||||||
|
{
|
||||||
|
Speed = OwnerCharacter->GetCharacterMovement()->GetMaxSpeed();
|
||||||
|
}
|
||||||
const float Delta = Speed * DeltaTime;
|
const float Delta = Speed * DeltaTime;
|
||||||
|
const float PrevDistance = CurrentDistance;
|
||||||
|
const bool bPrevForward = bMovingForward;
|
||||||
|
|
||||||
if (bMovingForward)
|
if (bMovingForward)
|
||||||
{
|
{
|
||||||
@ -159,6 +176,30 @@ void UPS_AI_Behavior_SplineFollowerComponent::TickComponent(
|
|||||||
CurrentDistance -= Delta;
|
CurrentDistance -= Delta;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log direction changes and significant distance jumps
|
||||||
|
if (bDrawDebug)
|
||||||
|
{
|
||||||
|
if (bMovingForward != bPrevForward)
|
||||||
|
{
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Warning,
|
||||||
|
TEXT("[%s] DIRECTION CHANGED: %s -> %s at dist=%.0f/%.0f"),
|
||||||
|
GetOwner() ? *GetOwner()->GetName() : TEXT("?"),
|
||||||
|
bPrevForward ? TEXT("FWD") : TEXT("BWD"),
|
||||||
|
bMovingForward ? TEXT("FWD") : TEXT("BWD"),
|
||||||
|
CurrentDistance, SplineLen);
|
||||||
|
}
|
||||||
|
|
||||||
|
const float DistJump = FMath::Abs(CurrentDistance - PrevDistance);
|
||||||
|
if (DistJump > Delta * 2.0f && DistJump > 10.0f)
|
||||||
|
{
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Warning,
|
||||||
|
TEXT("[%s] DIST JUMP: %.0f -> %.0f (delta=%.1f, expected=%.1f) fwd=%d"),
|
||||||
|
GetOwner() ? *GetOwner()->GetName() : TEXT("?"),
|
||||||
|
PrevDistance, CurrentDistance, DistJump, Delta,
|
||||||
|
(int32)bMovingForward);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ─── End of spline handling ─────────────────────────────────────────
|
// ─── End of spline handling ─────────────────────────────────────────
|
||||||
if (CurrentDistance >= SplineLen)
|
if (CurrentDistance >= SplineLen)
|
||||||
{
|
{
|
||||||
@ -168,8 +209,17 @@ void UPS_AI_Behavior_SplineFollowerComponent::TickComponent(
|
|||||||
}
|
}
|
||||||
else if (bReverseAtEnd)
|
else if (bReverseAtEnd)
|
||||||
{
|
{
|
||||||
CurrentDistance = SplineLen - (CurrentDistance - SplineLen);
|
// Clamp to the end — don't reverse the target point yet.
|
||||||
bMovingForward = false;
|
// Wait until the NPC is close enough, then reverse.
|
||||||
|
CurrentDistance = SplineLen;
|
||||||
|
const FVector EndPoint = CurrentSpline->GetWorldLocationAtDistance(SplineLen);
|
||||||
|
const float GapToEnd = FVector::Dist2D(GetOwner()->GetActorLocation(), EndPoint);
|
||||||
|
if (GapToEnd < 80.0f)
|
||||||
|
{
|
||||||
|
// NPC has caught up — now reverse
|
||||||
|
bMovingForward = false;
|
||||||
|
}
|
||||||
|
// Otherwise, target stays at the end and NPC walks toward it
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -187,8 +237,15 @@ void UPS_AI_Behavior_SplineFollowerComponent::TickComponent(
|
|||||||
}
|
}
|
||||||
else if (bReverseAtEnd)
|
else if (bReverseAtEnd)
|
||||||
{
|
{
|
||||||
CurrentDistance = -CurrentDistance;
|
// Clamp to the start — wait for NPC to catch up
|
||||||
bMovingForward = true;
|
CurrentDistance = 0.0f;
|
||||||
|
const FVector StartPoint = CurrentSpline->GetWorldLocationAtDistance(0.0f);
|
||||||
|
const float GapToStart = FVector::Dist2D(GetOwner()->GetActorLocation(), StartPoint);
|
||||||
|
if (GapToStart < 80.0f)
|
||||||
|
{
|
||||||
|
// NPC has caught up — now reverse
|
||||||
|
bMovingForward = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -211,14 +268,24 @@ void UPS_AI_Behavior_SplineFollowerComponent::TickComponent(
|
|||||||
ACharacter* Character = Cast<ACharacter>(Owner);
|
ACharacter* Character = Cast<ACharacter>(Owner);
|
||||||
if (Character && Character->GetCharacterMovement())
|
if (Character && Character->GetCharacterMovement())
|
||||||
{
|
{
|
||||||
// Compute velocity to reach the spline point
|
// Steer toward the spline point directly.
|
||||||
FVector DesiredVelocity = (TargetLocation - CurrentLocation) / FMath::Max(DeltaTime, 0.001f);
|
// The spline advances smoothly each frame so the target point moves along the curve.
|
||||||
|
// This gives natural cornering without drifting.
|
||||||
|
const FVector ToTarget = TargetLocation - CurrentLocation;
|
||||||
|
const float GapToSpline = ToTarget.Size2D();
|
||||||
|
|
||||||
// Clamp to avoid teleporting on large frame spikes
|
FVector DesiredVelocity;
|
||||||
const float MaxVel = Speed * 3.0f;
|
if (GapToSpline < 1.0f)
|
||||||
if (DesiredVelocity.SizeSquared() > MaxVel * MaxVel)
|
|
||||||
{
|
{
|
||||||
DesiredVelocity = DesiredVelocity.GetSafeNormal() * MaxVel;
|
// Exactly on the spline — use tangent to avoid zero velocity
|
||||||
|
const FVector SplineTangent = CurrentSpline->GetWorldDirectionAtDistance(CurrentDistance)
|
||||||
|
* (bMovingForward ? 1.0f : -1.0f);
|
||||||
|
DesiredVelocity = SplineTangent * Speed;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Steer toward the target point on the spline, clamped to Speed
|
||||||
|
DesiredVelocity = ToTarget.GetSafeNormal() * Speed;
|
||||||
}
|
}
|
||||||
|
|
||||||
Character->GetCharacterMovement()->RequestDirectMove(DesiredVelocity, /*bForceMaxSpeed=*/false);
|
Character->GetCharacterMovement()->RequestDirectMove(DesiredVelocity, /*bForceMaxSpeed=*/false);
|
||||||
@ -229,17 +296,55 @@ void UPS_AI_Behavior_SplineFollowerComponent::TickComponent(
|
|||||||
Owner->SetActorLocation(TargetLocation);
|
Owner->SetActorLocation(TargetLocation);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Smooth rotation — flip if going backward
|
// Smooth rotation — face toward the target point (not the spline tangent)
|
||||||
FRotator FinalTargetRot = TargetRotation;
|
// This prevents the NPC from looking too far into corners
|
||||||
if (!bMovingForward)
|
const FVector ToTargetDir = (TargetLocation - CurrentLocation).GetSafeNormal2D();
|
||||||
|
FRotator FinalTargetRot;
|
||||||
|
if (!ToTargetDir.IsNearlyZero())
|
||||||
{
|
{
|
||||||
FinalTargetRot.Yaw += 180.0f;
|
FinalTargetRot = ToTargetDir.Rotation();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Fallback to spline tangent if on top of the target
|
||||||
|
FinalTargetRot = TargetRotation;
|
||||||
|
if (!bMovingForward)
|
||||||
|
{
|
||||||
|
FinalTargetRot.Yaw += 180.0f;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const FRotator SmoothedRot = FMath::RInterpConstantTo(
|
const FRotator SmoothedRot = FMath::RInterpConstantTo(
|
||||||
CurrentRotation, FinalTargetRot, DeltaTime, RotationInterpSpeed);
|
CurrentRotation, FinalTargetRot, DeltaTime, RotationInterpSpeed);
|
||||||
Owner->SetActorRotation(FRotator(0.0f, SmoothedRot.Yaw, 0.0f)); // Only yaw
|
Owner->SetActorRotation(FRotator(0.0f, SmoothedRot.Yaw, 0.0f)); // Only yaw
|
||||||
|
|
||||||
|
// ─── Debug drawing ─────────────────────────────────────────────────
|
||||||
|
if (bDrawDebug)
|
||||||
|
{
|
||||||
|
const UWorld* World = GetWorld();
|
||||||
|
if (World)
|
||||||
|
{
|
||||||
|
// Target point on spline (green sphere)
|
||||||
|
DrawDebugSphere(World, TargetLocation, 15.0f, 8, FColor::Green, false, -1.0f, 0, 2.0f);
|
||||||
|
|
||||||
|
// Line from NPC to target point (yellow = gap)
|
||||||
|
DrawDebugLine(World, CurrentLocation, TargetLocation, FColor::Yellow, false, -1.0f, 0, 1.5f);
|
||||||
|
|
||||||
|
// Spline tangent direction (cyan arrow)
|
||||||
|
const FVector Tangent = CurrentSpline->GetWorldDirectionAtDistance(CurrentDistance)
|
||||||
|
* (bMovingForward ? 1.0f : -1.0f);
|
||||||
|
DrawDebugDirectionalArrow(World, TargetLocation, TargetLocation + Tangent * 150.0f,
|
||||||
|
20.0f, FColor::Cyan, false, -1.0f, 0, 2.0f);
|
||||||
|
|
||||||
|
// Gap distance text
|
||||||
|
const float DebugGap = FVector::Dist(CurrentLocation, TargetLocation);
|
||||||
|
DrawDebugString(World, CurrentLocation + FVector(0, 0, 100.0f),
|
||||||
|
FString::Printf(TEXT("Gap: %.0fcm Dist: %.0f/%.0f"),
|
||||||
|
DebugGap, CurrentDistance, CurrentSpline->GetSplineLength()),
|
||||||
|
nullptr, FColor::White, 0.0f, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Junction handling ──────────────────────────────────────────────
|
// ─── Junction handling ──────────────────────────────────────────────
|
||||||
HandleJunctions();
|
HandleJunctions();
|
||||||
}
|
}
|
||||||
@ -277,8 +382,31 @@ void UPS_AI_Behavior_SplineFollowerComponent::HandleJunctions()
|
|||||||
LastHandledJunctionIndex = i;
|
LastHandledJunctionIndex = i;
|
||||||
OnApproachingJunction.Broadcast(CurrentSpline, i, DistToJunction);
|
OnApproachingJunction.Broadcast(CurrentSpline, i, DistToJunction);
|
||||||
|
|
||||||
|
if (bDrawDebug)
|
||||||
|
{
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Log,
|
||||||
|
TEXT("[%s] Junction detected: idx=%d, dist along=%.0f, dist to=%.0fcm, worldPos=(%.0f,%.0f,%.0f)"),
|
||||||
|
GetOwner() ? *GetOwner()->GetName() : TEXT("?"),
|
||||||
|
i, J.DistanceOnThisSpline, DistToJunction,
|
||||||
|
J.WorldLocation.X, J.WorldLocation.Y, J.WorldLocation.Z);
|
||||||
|
}
|
||||||
|
|
||||||
if (bAutoChooseAtJunction)
|
if (bAutoChooseAtJunction)
|
||||||
{
|
{
|
||||||
|
// Only switch with a probability — don't always change at every junction
|
||||||
|
const float SwitchChance = 0.7f; // 70% chance to switch
|
||||||
|
const float Roll = FMath::FRand();
|
||||||
|
if (Roll > SwitchChance)
|
||||||
|
{
|
||||||
|
if (bDrawDebug)
|
||||||
|
{
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Log,
|
||||||
|
TEXT("[%s] Junction %d: skipped (roll=%.2f > chance=%.2f)"),
|
||||||
|
GetOwner() ? *GetOwner()->GetName() : TEXT("?"), i, Roll, SwitchChance);
|
||||||
|
}
|
||||||
|
break; // Stay on current spline
|
||||||
|
}
|
||||||
|
|
||||||
// Use SplineNetwork subsystem to choose
|
// Use SplineNetwork subsystem to choose
|
||||||
UPS_AI_Behavior_SplineNetwork* Network =
|
UPS_AI_Behavior_SplineNetwork* Network =
|
||||||
GetWorld()->GetSubsystem<UPS_AI_Behavior_SplineNetwork>();
|
GetWorld()->GetSubsystem<UPS_AI_Behavior_SplineNetwork>();
|
||||||
@ -301,7 +429,67 @@ void UPS_AI_Behavior_SplineFollowerComponent::HandleJunctions()
|
|||||||
|
|
||||||
if (ChosenSpline && ChosenSpline != CurrentSpline)
|
if (ChosenSpline && ChosenSpline != CurrentSpline)
|
||||||
{
|
{
|
||||||
SwitchToSpline(ChosenSpline, J.DistanceOnOtherSpline, bMovingForward);
|
// Only switch if the junction points are actually close in world space
|
||||||
|
const FVector CurrentPos = GetOwner()->GetActorLocation();
|
||||||
|
const FVector JunctionPos = J.WorldLocation;
|
||||||
|
const float WorldGap = FVector::Dist(CurrentPos, JunctionPos);
|
||||||
|
if (WorldGap > JunctionDetectionDistance * 2.0f)
|
||||||
|
{
|
||||||
|
if (bDrawDebug)
|
||||||
|
{
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Log,
|
||||||
|
TEXT("[%s] Junction %d: rejected (worldGap=%.0f > detect=%.0f)"),
|
||||||
|
GetOwner() ? *GetOwner()->GetName() : TEXT("?"),
|
||||||
|
i, WorldGap, JunctionDetectionDistance);
|
||||||
|
}
|
||||||
|
break; // Too far in world space
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check direction continuity: avoid U-turns
|
||||||
|
// Compare our current movement direction with the new spline's direction at the junction
|
||||||
|
const FVector CurrentDir = CurrentSpline->GetWorldDirectionAtDistance(CurrentDistance)
|
||||||
|
* (bMovingForward ? 1.0f : -1.0f);
|
||||||
|
|
||||||
|
const FVector NewDirForward = ChosenSpline->GetWorldDirectionAtDistance(J.DistanceOnOtherSpline);
|
||||||
|
const FVector NewDirBackward = -NewDirForward;
|
||||||
|
|
||||||
|
const float DotForward = FVector::DotProduct(CurrentDir, NewDirForward);
|
||||||
|
const float DotBackward = FVector::DotProduct(CurrentDir, NewDirBackward);
|
||||||
|
|
||||||
|
// Choose the direction on the new spline that best continues our current heading
|
||||||
|
bool bNewForward;
|
||||||
|
float BestDot;
|
||||||
|
if (DotForward >= DotBackward)
|
||||||
|
{
|
||||||
|
bNewForward = true;
|
||||||
|
BestDot = DotForward;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
bNewForward = false;
|
||||||
|
BestDot = DotBackward;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only switch if the direction change is less than ~135° (dot > -0.7)
|
||||||
|
// This prevents full U-turns but allows wide turns at crossings
|
||||||
|
if (BestDot < -0.7f)
|
||||||
|
{
|
||||||
|
if (bDrawDebug)
|
||||||
|
{
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Log,
|
||||||
|
TEXT("[%s] Junction %d: rejected U-turn (dot=%.2f)"),
|
||||||
|
GetOwner() ? *GetOwner()->GetName() : TEXT("?"), i, BestDot);
|
||||||
|
}
|
||||||
|
break; // Would cause a near-180° U-turn, skip
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Verbose,
|
||||||
|
TEXT("[%s] Junction switch: %s -> %s (dot=%.2f, dir=%s, worldGap=%.0fcm)"),
|
||||||
|
GetOwner() ? *GetOwner()->GetName() : TEXT("?"),
|
||||||
|
*CurrentSpline->GetName(), *ChosenSpline->GetName(),
|
||||||
|
BestDot, bNewForward ? TEXT("fwd") : TEXT("bwd"),
|
||||||
|
FVector::Dist(CurrentPos, JunctionPos));
|
||||||
|
SwitchToSpline(ChosenSpline, J.DistanceOnOtherSpline, bNewForward);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -102,6 +102,12 @@ FRotator APS_AI_Behavior_SplinePath::GetWorldRotationAtDistance(float Distance)
|
|||||||
return SplineComp->GetRotationAtDistanceAlongSpline(Distance, ESplineCoordinateSpace::World);
|
return SplineComp->GetRotationAtDistanceAlongSpline(Distance, ESplineCoordinateSpace::World);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FVector APS_AI_Behavior_SplinePath::GetWorldDirectionAtDistance(float Distance) const
|
||||||
|
{
|
||||||
|
if (!SplineComp) return FVector::ForwardVector;
|
||||||
|
return SplineComp->GetDirectionAtDistanceAlongSpline(Distance, ESplineCoordinateSpace::World);
|
||||||
|
}
|
||||||
|
|
||||||
void APS_AI_Behavior_SplinePath::UpdateSplineVisualization()
|
void APS_AI_Behavior_SplinePath::UpdateSplineVisualization()
|
||||||
{
|
{
|
||||||
if (!SplineComp) return;
|
if (!SplineComp) return;
|
||||||
|
|||||||
@ -7,11 +7,14 @@
|
|||||||
#include "PS_AI_Behavior_BTTask_Patrol.generated.h"
|
#include "PS_AI_Behavior_BTTask_Patrol.generated.h"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* BT Task: Navigate to the next patrol waypoint.
|
* BT Task: Patrol around the NPC's home location.
|
||||||
* Reads PatrolIndex from Blackboard, navigates to the corresponding point in
|
|
||||||
* the AIController's PatrolPoints array, then increments the index (cyclic).
|
|
||||||
*
|
*
|
||||||
* Optional random wait at each waypoint.
|
* Two modes (automatic):
|
||||||
|
* - If AIController has PatrolPoints → cycles through them.
|
||||||
|
* - If no PatrolPoints → picks a random NavMesh point within PatrolRadius
|
||||||
|
* around the HomeLocation (from Blackboard).
|
||||||
|
*
|
||||||
|
* Waits a random time at each waypoint before moving to the next.
|
||||||
*/
|
*/
|
||||||
UCLASS(meta = (DisplayName = "PS AI: Patrol"))
|
UCLASS(meta = (DisplayName = "PS AI: Patrol"))
|
||||||
class PS_AI_BEHAVIOR_API UPS_AI_Behavior_BTTask_Patrol : public UBTTaskNode
|
class PS_AI_BEHAVIOR_API UPS_AI_Behavior_BTTask_Patrol : public UBTTaskNode
|
||||||
@ -33,6 +36,20 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, Category = "Patrol", meta = (ClampMin = "0.0"))
|
UPROPERTY(EditAnywhere, Category = "Patrol", meta = (ClampMin = "0.0"))
|
||||||
float MaxWaitTime = 4.0f;
|
float MaxWaitTime = 4.0f;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Radius around HomeLocation to pick random patrol points (cm).
|
||||||
|
* Only used when no manual PatrolPoints are defined.
|
||||||
|
*/
|
||||||
|
UPROPERTY(EditAnywhere, Category = "Patrol|Auto", meta = (ClampMin = "200.0"))
|
||||||
|
float PatrolRadius = 1500.0f;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimum distance from current position for a random patrol point (cm).
|
||||||
|
* Prevents picking a point right next to the NPC.
|
||||||
|
*/
|
||||||
|
UPROPERTY(EditAnywhere, Category = "Patrol|Auto", meta = (ClampMin = "100.0"))
|
||||||
|
float MinPatrolDistance = 300.0f;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
|
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
|
||||||
virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
|
virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
|
||||||
@ -40,7 +57,6 @@ protected:
|
|||||||
virtual FString GetStaticDescription() const override;
|
virtual FString GetStaticDescription() const override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
/** Per-instance memory. */
|
|
||||||
struct FPatrolMemory
|
struct FPatrolMemory
|
||||||
{
|
{
|
||||||
float WaitRemaining = 0.0f;
|
float WaitRemaining = 0.0f;
|
||||||
@ -49,4 +65,11 @@ private:
|
|||||||
};
|
};
|
||||||
|
|
||||||
virtual uint16 GetInstanceMemorySize() const override { return sizeof(FPatrolMemory); }
|
virtual uint16 GetInstanceMemorySize() const override { return sizeof(FPatrolMemory); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a random navigable point within PatrolRadius of HomeLocation.
|
||||||
|
* Ensures the point is at least MinPatrolDistance from the NPC.
|
||||||
|
*/
|
||||||
|
bool FindRandomPatrolPoint(const UWorld* World, const FVector& HomeLoc,
|
||||||
|
const FVector& CurrentLoc, FVector& OutPoint) const;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -24,7 +24,7 @@ class UBillboardComponent;
|
|||||||
* - Crouch flag: NPC should crouch at this cover.
|
* - Crouch flag: NPC should crouch at this cover.
|
||||||
* - Editor: color-coded (blue=Cover, yellow=HidingSpot), arrow shows facing.
|
* - Editor: color-coded (blue=Cover, yellow=HidingSpot), arrow shows facing.
|
||||||
*/
|
*/
|
||||||
UCLASS(BlueprintType, Blueprintable, meta = (DisplayName = "PS AI Cover Point"))
|
UCLASS(BlueprintType, Blueprintable, Placeable, meta = (DisplayName = "PS AI Cover Point"))
|
||||||
class PS_AI_BEHAVIOR_API APS_AI_Behavior_CoverPoint : public AActor
|
class PS_AI_BEHAVIOR_API APS_AI_Behavior_CoverPoint : public AActor
|
||||||
{
|
{
|
||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
|
|||||||
@ -71,7 +71,11 @@ public:
|
|||||||
* If false, stop and fire OnSplineEndReached.
|
* If false, stop and fire OnSplineEndReached.
|
||||||
*/
|
*/
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spline Follower")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spline Follower")
|
||||||
bool bReverseAtEnd = false;
|
bool bReverseAtEnd = true;
|
||||||
|
|
||||||
|
/** Draw debug info: target point on spline, direction, gap distance. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spline Follower|Debug")
|
||||||
|
bool bDrawDebug = false;
|
||||||
|
|
||||||
// ─── Runtime State ──────────────────────────────────────────────────
|
// ─── Runtime State ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@ -44,7 +44,7 @@ struct FPS_AI_Behavior_SplineJunction
|
|||||||
* and lets NPCs switch between paths at those points.
|
* and lets NPCs switch between paths at those points.
|
||||||
* - Supports bidirectional travel by default.
|
* - Supports bidirectional travel by default.
|
||||||
*/
|
*/
|
||||||
UCLASS(BlueprintType, Blueprintable, meta = (DisplayName = "PS AI Spline Path"))
|
UCLASS(BlueprintType, Blueprintable, Placeable, meta = (DisplayName = "PS AI Spline Path"))
|
||||||
class PS_AI_BEHAVIOR_API APS_AI_Behavior_SplinePath : public AActor
|
class PS_AI_BEHAVIOR_API APS_AI_Behavior_SplinePath : public AActor
|
||||||
{
|
{
|
||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
@ -131,6 +131,10 @@ public:
|
|||||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Spline")
|
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Spline")
|
||||||
FRotator GetWorldRotationAtDistance(float Distance) const;
|
FRotator GetWorldRotationAtDistance(float Distance) const;
|
||||||
|
|
||||||
|
/** Get world-space tangent direction at a distance along the spline. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Spline")
|
||||||
|
FVector GetWorldDirectionAtDistance(float Distance) const;
|
||||||
|
|
||||||
#if WITH_EDITOR
|
#if WITH_EDITOR
|
||||||
virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
|
virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@ -0,0 +1,29 @@
|
|||||||
|
// Copyright Asterion. All Rights Reserved.
|
||||||
|
|
||||||
|
#include "PS_AI_Behavior_PersonalityProfileFactory.h"
|
||||||
|
#include "PS_AI_Behavior_PersonalityProfile.h"
|
||||||
|
|
||||||
|
UPS_AI_Behavior_PersonalityProfileFactory::UPS_AI_Behavior_PersonalityProfileFactory()
|
||||||
|
{
|
||||||
|
bCreateNew = true;
|
||||||
|
bEditAfterNew = true;
|
||||||
|
SupportedClass = UPS_AI_Behavior_PersonalityProfile::StaticClass();
|
||||||
|
}
|
||||||
|
|
||||||
|
UObject* UPS_AI_Behavior_PersonalityProfileFactory::FactoryCreateNew(
|
||||||
|
UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags,
|
||||||
|
UObject* Context, FFeedbackContext* Warn)
|
||||||
|
{
|
||||||
|
return NewObject<UPS_AI_Behavior_PersonalityProfile>(InParent, InClass, InName, Flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
FText UPS_AI_Behavior_PersonalityProfileFactory::GetDisplayName() const
|
||||||
|
{
|
||||||
|
return FText::FromString(TEXT("Personality Profile"));
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32 UPS_AI_Behavior_PersonalityProfileFactory::GetMenuCategories() const
|
||||||
|
{
|
||||||
|
// EAssetTypeCategories::Misc = 1
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
// Copyright Asterion. All Rights Reserved.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Factories/Factory.h"
|
||||||
|
#include "PS_AI_Behavior_PersonalityProfileFactory.generated.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory to create PersonalityProfile Data Assets from the Content Browser.
|
||||||
|
* Right-click → PS AI Behavior → Personality Profile
|
||||||
|
*/
|
||||||
|
UCLASS()
|
||||||
|
class UPS_AI_Behavior_PersonalityProfileFactory : public UFactory
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPS_AI_Behavior_PersonalityProfileFactory();
|
||||||
|
|
||||||
|
virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent,
|
||||||
|
FName InName, EObjectFlags Flags, UObject* Context,
|
||||||
|
FFeedbackContext* Warn) override;
|
||||||
|
|
||||||
|
virtual FText GetDisplayName() const override;
|
||||||
|
virtual uint32 GetMenuCategories() const override;
|
||||||
|
};
|
||||||
@ -22,4 +22,7 @@ private:
|
|||||||
|
|
||||||
void RegisterSplinePanel();
|
void RegisterSplinePanel();
|
||||||
void UnregisterSplinePanel();
|
void UnregisterSplinePanel();
|
||||||
|
|
||||||
|
/** Create or update the BB_Behavior Blackboard asset with proper enum keys. */
|
||||||
|
void EnsureBlackboardAsset();
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user