Fix combat cycle, spline resume, team identity, and debug system
- Add TeamComponent for player pawn team identity (Role-based: Civilian/Enemy/Protector) - Add IsTargetActorValid to interface for dead target filtering - Fix GetTeamAttitudeTowards to check IGenericTeamAgentInterface + TeamComponent - Guarantee BehaviorStopAttack via OnTaskFinished (all exit paths) - Prevent Combat state without ThreatActor (stay Alerted until perception catches up) - Resume spline at closest point from current position after combat - Sync CurrentSpline and SplineProgress to Blackboard in FollowSpline tick - Auto-detect Patrol state when NPC has a spline (fixes Idle speed=0 blocking movement) - Add per-component debug toggles (Personality + SplineFollower independent) - Use AddMovementInput instead of RequestDirectMove for reliable post-combat movement - Add bTickInEditor for Personality debug in Simulate mode Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0c4c409ebc
commit
e7c3598dce
@ -55,7 +55,18 @@ void UPS_AI_Behavior_BTService_EvaluateReaction::TickNode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ─── Evaluate and apply the reaction ────────────────────────────────
|
// ─── Evaluate and apply the reaction ────────────────────────────────
|
||||||
const EPS_AI_Behavior_State NewState = Personality->ApplyReaction();
|
EPS_AI_Behavior_State NewState = Personality->ApplyReaction();
|
||||||
|
|
||||||
|
// Don't enter Combat or TakingCover without a valid ThreatActor in BB
|
||||||
|
if (NewState == EPS_AI_Behavior_State::Combat || NewState == EPS_AI_Behavior_State::TakingCover)
|
||||||
|
{
|
||||||
|
const AActor* ThreatActor = Cast<AActor>(BB->GetValueAsObject(PS_AI_Behavior_BB::ThreatActor));
|
||||||
|
if (!ThreatActor)
|
||||||
|
{
|
||||||
|
// Threat level says fight, but no target yet — stay Alerted until perception catches up
|
||||||
|
NewState = EPS_AI_Behavior_State::Alerted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Write to Blackboard
|
// Write to Blackboard
|
||||||
AIC->SetBehaviorState(NewState);
|
AIC->SetBehaviorState(NewState);
|
||||||
|
|||||||
@ -72,11 +72,12 @@ void UPS_AI_Behavior_BTService_UpdateThreat::TickNode(
|
|||||||
BB->SetValueAsObject(PS_AI_Behavior_BB::ThreatActor, ThreatActor);
|
BB->SetValueAsObject(PS_AI_Behavior_BB::ThreatActor, ThreatActor);
|
||||||
BB->SetValueAsVector(PS_AI_Behavior_BB::ThreatLocation, ThreatActor->GetActorLocation());
|
BB->SetValueAsVector(PS_AI_Behavior_BB::ThreatLocation, ThreatActor->GetActorLocation());
|
||||||
}
|
}
|
||||||
else if (FinalThreat <= 0.01f)
|
else
|
||||||
{
|
{
|
||||||
// Clear threat data when fully decayed
|
// No valid threat — clear BB and force threat to zero
|
||||||
BB->ClearValue(PS_AI_Behavior_BB::ThreatActor);
|
BB->ClearValue(PS_AI_Behavior_BB::ThreatActor);
|
||||||
BB->ClearValue(PS_AI_Behavior_BB::ThreatLocation);
|
BB->ClearValue(PS_AI_Behavior_BB::ThreatLocation);
|
||||||
|
BB->SetValueAsFloat(PS_AI_Behavior_BB::ThreatLevel, 0.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync to PersonalityComponent
|
// Sync to PersonalityComponent
|
||||||
|
|||||||
@ -31,6 +31,17 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_Attack::ExecuteTask(
|
|||||||
}
|
}
|
||||||
|
|
||||||
APawn* Pawn = AIC->GetPawn();
|
APawn* Pawn = AIC->GetPawn();
|
||||||
|
|
||||||
|
// Validate target before starting attack
|
||||||
|
if (Pawn && Pawn->Implements<UPS_AI_Behavior_Interface>())
|
||||||
|
{
|
||||||
|
if (!IPS_AI_Behavior_Interface::Execute_IsTargetActorValid(Pawn, Target))
|
||||||
|
{
|
||||||
|
BB->ClearValue(PS_AI_Behavior_BB::ThreatActor);
|
||||||
|
BB->SetValueAsFloat(PS_AI_Behavior_BB::ThreatLevel, 0.0f);
|
||||||
|
return EBTNodeResult::Failed;
|
||||||
|
}
|
||||||
|
}
|
||||||
FAttackMemory* Memory = reinterpret_cast<FAttackMemory*>(NodeMemory);
|
FAttackMemory* Memory = reinterpret_cast<FAttackMemory*>(NodeMemory);
|
||||||
Memory->bMovingToTarget = false;
|
Memory->bMovingToTarget = false;
|
||||||
Memory->bAttacking = false;
|
Memory->bAttacking = false;
|
||||||
@ -72,10 +83,26 @@ void UPS_AI_Behavior_BTTask_Attack::TickTask(
|
|||||||
AActor* Target = BB ? Cast<AActor>(BB->GetValueAsObject(PS_AI_Behavior_BB::ThreatActor)) : nullptr;
|
AActor* Target = BB ? Cast<AActor>(BB->GetValueAsObject(PS_AI_Behavior_BB::ThreatActor)) : nullptr;
|
||||||
if (!Target)
|
if (!Target)
|
||||||
{
|
{
|
||||||
|
BB->SetValueAsFloat(PS_AI_Behavior_BB::ThreatLevel, 0.0f);
|
||||||
FinishLatentTask(OwnerComp, EBTNodeResult::Failed);
|
FinishLatentTask(OwnerComp, EBTNodeResult::Failed);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if target is still valid (alive, not despawning) via interface
|
||||||
|
APawn* Pawn = AIC->GetPawn();
|
||||||
|
if (Pawn && Pawn->Implements<UPS_AI_Behavior_Interface>())
|
||||||
|
{
|
||||||
|
if (!IPS_AI_Behavior_Interface::Execute_IsTargetActorValid(Pawn, Target))
|
||||||
|
{
|
||||||
|
// Target is dead/invalid — clear BB, StopAttack called by OnTaskFinished
|
||||||
|
AIC->StopMovement();
|
||||||
|
BB->ClearValue(PS_AI_Behavior_BB::ThreatActor);
|
||||||
|
BB->SetValueAsFloat(PS_AI_Behavior_BB::ThreatLevel, 0.0f);
|
||||||
|
FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
FAttackMemory* Memory = reinterpret_cast<FAttackMemory*>(NodeMemory);
|
FAttackMemory* Memory = reinterpret_cast<FAttackMemory*>(NodeMemory);
|
||||||
|
|
||||||
// Keep moving toward target if out of range
|
// Keep moving toward target if out of range
|
||||||
@ -96,15 +123,26 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_Attack::AbortTask(
|
|||||||
if (AIC)
|
if (AIC)
|
||||||
{
|
{
|
||||||
AIC->StopMovement();
|
AIC->StopMovement();
|
||||||
|
}
|
||||||
|
// StopAttack is called by OnTaskFinished (covers all exit paths)
|
||||||
|
return EBTNodeResult::Aborted;
|
||||||
|
}
|
||||||
|
|
||||||
// Tell the Pawn to stop attacking
|
void UPS_AI_Behavior_BTTask_Attack::OnTaskFinished(
|
||||||
|
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, EBTNodeResult::Type TaskResult)
|
||||||
|
{
|
||||||
|
// Always stop attacking when the task ends, regardless of how it ended
|
||||||
|
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp.GetAIOwner());
|
||||||
|
if (AIC)
|
||||||
|
{
|
||||||
APawn* Pawn = AIC->GetPawn();
|
APawn* Pawn = AIC->GetPawn();
|
||||||
if (Pawn && Pawn->Implements<UPS_AI_Behavior_Interface>())
|
if (Pawn && Pawn->Implements<UPS_AI_Behavior_Interface>())
|
||||||
{
|
{
|
||||||
IPS_AI_Behavior_Interface::Execute_BehaviorStopAttack(Pawn);
|
IPS_AI_Behavior_Interface::Execute_BehaviorStopAttack(Pawn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return EBTNodeResult::Aborted;
|
|
||||||
|
Super::OnTaskFinished(OwnerComp, NodeMemory, TaskResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
FString UPS_AI_Behavior_BTTask_Attack::GetStaticDescription() const
|
FString UPS_AI_Behavior_BTTask_Attack::GetStaticDescription() const
|
||||||
|
|||||||
@ -31,6 +31,12 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_FindAndFollowSpline::ExecuteTask(
|
|||||||
return EBTNodeResult::Failed;
|
return EBTNodeResult::Failed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Debug: log state on entry
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] FindAndFollowSpline: bIsFollowing=%d CurrentSpline=%s"),
|
||||||
|
*AIC->GetName(),
|
||||||
|
(int32)Follower->bIsFollowing,
|
||||||
|
Follower->CurrentSpline ? *Follower->CurrentSpline->GetName() : TEXT("null"));
|
||||||
|
|
||||||
// If already following a spline, don't re-search — just succeed immediately
|
// If already following a spline, don't re-search — just succeed immediately
|
||||||
// The Follow Spline task will continue the movement
|
// The Follow Spline task will continue the movement
|
||||||
if (Follower->bIsFollowing && Follower->CurrentSpline)
|
if (Follower->bIsFollowing && Follower->CurrentSpline)
|
||||||
@ -38,6 +44,68 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_FindAndFollowSpline::ExecuteTask(
|
|||||||
return EBTNodeResult::Succeeded;
|
return EBTNodeResult::Succeeded;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If paused (e.g. after combat abort) but still has a valid spline, resume it
|
||||||
|
// Find the closest point on the spline from NPC's CURRENT position (not old pause point)
|
||||||
|
if (!Follower->bIsFollowing && Follower->CurrentSpline)
|
||||||
|
{
|
||||||
|
const FVector PawnLoc = AIC->GetPawn()->GetActorLocation();
|
||||||
|
float ClosestDist = 0.0f;
|
||||||
|
FVector ClosestPoint = FVector::ZeroVector;
|
||||||
|
const float GapToSpline = Follower->CurrentSpline->GetClosestPointOnSpline(
|
||||||
|
PawnLoc, ClosestDist, ClosestPoint);
|
||||||
|
|
||||||
|
// Determine direction from pawn forward vs spline tangent at closest point
|
||||||
|
const FVector PawnFwd = AIC->GetPawn()->GetActorForwardVector();
|
||||||
|
const FVector SplineDir = Follower->CurrentSpline->GetWorldDirectionAtDistance(ClosestDist);
|
||||||
|
const bool bForward = FVector::DotProduct(PawnFwd, SplineDir) >= 0.0f;
|
||||||
|
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Log,
|
||||||
|
TEXT("[%s] FindAndFollowSpline: resuming spline '%s' at closest point (gap=%.0fcm, dist=%.0f, bWalkToSpline=%d, AcceptanceRadius=%.0f)"),
|
||||||
|
*AIC->GetName(), *Follower->CurrentSpline->GetName(), GapToSpline, ClosestDist,
|
||||||
|
(int32)bWalkToSpline, AcceptanceRadius);
|
||||||
|
|
||||||
|
if (bWalkToSpline && GapToSpline > AcceptanceRadius)
|
||||||
|
{
|
||||||
|
// Walk to closest spline point via NavMesh
|
||||||
|
Follower->CurrentDistance = ClosestDist;
|
||||||
|
const EPathFollowingRequestResult::Type Result = AIC->MoveToLocation(
|
||||||
|
ClosestPoint, AcceptanceRadius, /*bStopOnOverlap=*/true,
|
||||||
|
/*bUsePathfinding=*/true, /*bProjectDestinationToNavigation=*/true,
|
||||||
|
/*bCanStrafe=*/false);
|
||||||
|
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Log,
|
||||||
|
TEXT("[%s] FindAndFollowSpline: MoveToLocation result=%d (0=Failed, 1=AlreadyAtGoal, 2=RequestSuccessful)"),
|
||||||
|
*AIC->GetName(), (int32)Result);
|
||||||
|
|
||||||
|
if (Result == EPathFollowingRequestResult::AlreadyAtGoal)
|
||||||
|
{
|
||||||
|
Follower->StartFollowingAtDistance(Follower->CurrentSpline, ClosestDist, bForward);
|
||||||
|
return EBTNodeResult::Succeeded;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Result != EPathFollowingRequestResult::Failed)
|
||||||
|
{
|
||||||
|
FFindSplineMemory* Memory = reinterpret_cast<FFindSplineMemory*>(NodeMemory);
|
||||||
|
Memory->bMovingToSpline = true;
|
||||||
|
return EBTNodeResult::InProgress;
|
||||||
|
}
|
||||||
|
// Pathfinding failed — fall through to full re-search below
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Warning,
|
||||||
|
TEXT("[%s] FindAndFollowSpline: pathfinding FAILED to reach spline, falling through to re-search"),
|
||||||
|
*AIC->GetName());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Close enough — clear any residual movement request and start following
|
||||||
|
AIC->StopMovement();
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Log,
|
||||||
|
TEXT("[%s] FindAndFollowSpline: close enough, StartFollowingAtDistance(dist=%.0f, fwd=%d)"),
|
||||||
|
*AIC->GetName(), ClosestDist, (int32)bForward);
|
||||||
|
Follower->StartFollowingAtDistance(Follower->CurrentSpline, ClosestDist, bForward);
|
||||||
|
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();
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
#include "PS_AI_Behavior_SplineFollowerComponent.h"
|
#include "PS_AI_Behavior_SplineFollowerComponent.h"
|
||||||
#include "PS_AI_Behavior_SplinePath.h"
|
#include "PS_AI_Behavior_SplinePath.h"
|
||||||
#include "PS_AI_Behavior_Definitions.h"
|
#include "PS_AI_Behavior_Definitions.h"
|
||||||
|
#include "BehaviorTree/BlackboardComponent.h"
|
||||||
|
|
||||||
UPS_AI_Behavior_BTTask_FollowSpline::UPS_AI_Behavior_BTTask_FollowSpline()
|
UPS_AI_Behavior_BTTask_FollowSpline::UPS_AI_Behavior_BTTask_FollowSpline()
|
||||||
{
|
{
|
||||||
@ -61,6 +62,19 @@ void UPS_AI_Behavior_BTTask_FollowSpline::TickTask(
|
|||||||
{
|
{
|
||||||
UPS_AI_Behavior_SplineFollowerComponent* FollowerCheck =
|
UPS_AI_Behavior_SplineFollowerComponent* FollowerCheck =
|
||||||
AICCheck->GetPawn()->FindComponentByClass<UPS_AI_Behavior_SplineFollowerComponent>();
|
AICCheck->GetPawn()->FindComponentByClass<UPS_AI_Behavior_SplineFollowerComponent>();
|
||||||
|
|
||||||
|
// Sync spline info to Blackboard
|
||||||
|
if (FollowerCheck)
|
||||||
|
{
|
||||||
|
if (UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent())
|
||||||
|
{
|
||||||
|
BB->SetValueAsObject(PS_AI_Behavior_BB::CurrentSpline,
|
||||||
|
FollowerCheck->CurrentSpline);
|
||||||
|
BB->SetValueAsFloat(PS_AI_Behavior_BB::SplineProgress,
|
||||||
|
FollowerCheck->GetProgress());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (FollowerCheck && !FollowerCheck->bIsFollowing)
|
if (FollowerCheck && !FollowerCheck->bIsFollowing)
|
||||||
{
|
{
|
||||||
FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
|
FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
|
||||||
@ -112,6 +126,14 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_FollowSpline::AbortTask(
|
|||||||
Follower->PauseFollowing();
|
Follower->PauseFollowing();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear spline info from BB
|
||||||
|
if (UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent())
|
||||||
|
{
|
||||||
|
BB->ClearValue(PS_AI_Behavior_BB::CurrentSpline);
|
||||||
|
BB->SetValueAsFloat(PS_AI_Behavior_BB::SplineProgress, 0.0f);
|
||||||
|
}
|
||||||
|
|
||||||
return EBTNodeResult::Aborted;
|
return EBTNodeResult::Aborted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
#include "PS_AI_Behavior_Interface.h"
|
#include "PS_AI_Behavior_Interface.h"
|
||||||
#include "PS_AI_Behavior_PerceptionComponent.h"
|
#include "PS_AI_Behavior_PerceptionComponent.h"
|
||||||
#include "PS_AI_Behavior_PersonalityComponent.h"
|
#include "PS_AI_Behavior_PersonalityComponent.h"
|
||||||
|
#include "PS_AI_Behavior_TeamComponent.h"
|
||||||
#include "PS_AI_Behavior_PersonalityProfile.h"
|
#include "PS_AI_Behavior_PersonalityProfile.h"
|
||||||
#include "BehaviorTree/BehaviorTree.h"
|
#include "BehaviorTree/BehaviorTree.h"
|
||||||
#include "BehaviorTree/BlackboardComponent.h"
|
#include "BehaviorTree/BlackboardComponent.h"
|
||||||
@ -275,12 +276,23 @@ ETeamAttitude::Type APS_AI_Behavior_AIController::GetTeamAttitudeTowards(const A
|
|||||||
|
|
||||||
if (OtherPawn)
|
if (OtherPawn)
|
||||||
{
|
{
|
||||||
// Check via AIController (NPC with our behavior system)
|
// 1) Check controller's IGenericTeamAgentInterface (AI controllers, custom player controllers)
|
||||||
if (const AAIController* OtherAIC = Cast<AAIController>(OtherPawn->GetController()))
|
if (const AController* OtherController = OtherPawn->GetController())
|
||||||
{
|
{
|
||||||
OtherTeam = OtherAIC->GetGenericTeamId().GetId();
|
if (const IGenericTeamAgentInterface* TeamAgent = Cast<IGenericTeamAgentInterface>(OtherController))
|
||||||
|
{
|
||||||
|
OtherTeam = TeamAgent->GetGenericTeamId().GetId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Fallback: check for our TeamComponent on the Pawn (player characters, etc.)
|
||||||
|
if (OtherTeam == FGenericTeamId::NoTeam)
|
||||||
|
{
|
||||||
|
if (const UPS_AI_Behavior_TeamComponent* TeamComp = OtherPawn->FindComponentByClass<UPS_AI_Behavior_TeamComponent>())
|
||||||
|
{
|
||||||
|
OtherTeam = TeamComp->GetGenericTeamId().GetId();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Players or other pawns without AIController → NoTeam (Neutral)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NoTeam (255) → Neutral
|
// NoTeam (255) → Neutral
|
||||||
|
|||||||
@ -180,12 +180,15 @@ AActor* UPS_AI_Behavior_PerceptionComponent::GetHighestThreatActor(
|
|||||||
AActor* BestThreat = nullptr;
|
AActor* BestThreat = nullptr;
|
||||||
float BestScore = -1.0f;
|
float BestScore = -1.0f;
|
||||||
|
|
||||||
|
// Get our Pawn for IsTargetActorValid checks
|
||||||
|
const AAIController* AIC = Cast<AAIController>(Owner);
|
||||||
|
APawn* MyPawn = AIC ? AIC->GetPawn() : Cast<APawn>(const_cast<AActor*>(Owner));
|
||||||
|
|
||||||
for (AActor* Actor : PerceivedActors)
|
for (AActor* Actor : PerceivedActors)
|
||||||
{
|
{
|
||||||
if (!Actor || Actor == Owner) continue;
|
if (!Actor || Actor == Owner) continue;
|
||||||
|
|
||||||
// Skip self (when owner is AIController, also skip own pawn)
|
// Skip self (when owner is AIController, also skip own pawn)
|
||||||
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)
|
// Skip non-hostile actors (only Hostile actors are valid threats)
|
||||||
@ -198,6 +201,15 @@ AActor* UPS_AI_Behavior_PerceptionComponent::GetHighestThreatActor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip invalid targets (dead, despawning, etc.) via interface
|
||||||
|
if (MyPawn && MyPawn->Implements<UPS_AI_Behavior_Interface>())
|
||||||
|
{
|
||||||
|
if (!IPS_AI_Behavior_Interface::Execute_IsTargetActorValid(MyPawn, Actor))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Classify this actor ────────────────────────────────────────
|
// ─── Classify this actor ────────────────────────────────────────
|
||||||
const EPS_AI_Behavior_TargetType ActorType = ClassifyActor(Actor);
|
const EPS_AI_Behavior_TargetType ActorType = ClassifyActor(Actor);
|
||||||
|
|
||||||
@ -268,8 +280,9 @@ 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
|
// Get our AIController and Pawn for checks
|
||||||
const AAIController* AIC = Cast<AAIController>(Owner);
|
const AAIController* AIC = Cast<AAIController>(Owner);
|
||||||
|
APawn* MyPawn = AIC ? AIC->GetPawn() : Cast<APawn>(const_cast<AActor*>(Owner));
|
||||||
|
|
||||||
for (AActor* Actor : PerceivedActors)
|
for (AActor* Actor : PerceivedActors)
|
||||||
{
|
{
|
||||||
@ -285,6 +298,15 @@ float UPS_AI_Behavior_PerceptionComponent::CalculateThreatLevel()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip invalid targets (dead, despawning, etc.) via interface
|
||||||
|
if (MyPawn && MyPawn->Implements<UPS_AI_Behavior_Interface>())
|
||||||
|
{
|
||||||
|
if (!IPS_AI_Behavior_Interface::Execute_IsTargetActorValid(MyPawn, Actor))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
float ActorThreat = 0.0f;
|
float ActorThreat = 0.0f;
|
||||||
const float Dist = FVector::Dist(OwnerLoc, Actor->GetActorLocation());
|
const float Dist = FVector::Dist(OwnerLoc, Actor->GetActorLocation());
|
||||||
|
|
||||||
|
|||||||
@ -3,11 +3,19 @@
|
|||||||
#include "PS_AI_Behavior_PersonalityComponent.h"
|
#include "PS_AI_Behavior_PersonalityComponent.h"
|
||||||
#include "PS_AI_Behavior_Interface.h"
|
#include "PS_AI_Behavior_Interface.h"
|
||||||
#include "PS_AI_Behavior_PersonalityProfile.h"
|
#include "PS_AI_Behavior_PersonalityProfile.h"
|
||||||
|
#include "PS_AI_Behavior_AIController.h"
|
||||||
|
#include "PS_AI_Behavior_SplineFollowerComponent.h"
|
||||||
|
#include "PS_AI_Behavior_SplinePath.h"
|
||||||
|
#include "BehaviorTree/BlackboardComponent.h"
|
||||||
#include "Net/UnrealNetwork.h"
|
#include "Net/UnrealNetwork.h"
|
||||||
|
#include "DrawDebugHelpers.h"
|
||||||
|
#include "Kismet/KismetSystemLibrary.h"
|
||||||
|
#include "AIController.h"
|
||||||
|
|
||||||
UPS_AI_Behavior_PersonalityComponent::UPS_AI_Behavior_PersonalityComponent()
|
UPS_AI_Behavior_PersonalityComponent::UPS_AI_Behavior_PersonalityComponent()
|
||||||
{
|
{
|
||||||
PrimaryComponentTick.bCanEverTick = false;
|
PrimaryComponentTick.bCanEverTick = true;
|
||||||
|
bTickInEditor = true; // Required for Simulate In Editor
|
||||||
SetIsReplicatedByDefault(true);
|
SetIsReplicatedByDefault(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,10 +109,19 @@ EPS_AI_Behavior_State UPS_AI_Behavior_PersonalityComponent::EvaluateReaction() c
|
|||||||
return EPS_AI_Behavior_State::Alerted;
|
return EPS_AI_Behavior_State::Alerted;
|
||||||
}
|
}
|
||||||
|
|
||||||
// No threat — maintain patrol or idle
|
// No threat — patrol if following a spline, otherwise idle
|
||||||
return (CurrentState == EPS_AI_Behavior_State::Patrol)
|
if (const AActor* Owner = GetOwner())
|
||||||
? EPS_AI_Behavior_State::Patrol
|
{
|
||||||
: EPS_AI_Behavior_State::Idle;
|
if (const UPS_AI_Behavior_SplineFollowerComponent* Spline =
|
||||||
|
Owner->FindComponentByClass<UPS_AI_Behavior_SplineFollowerComponent>())
|
||||||
|
{
|
||||||
|
if (Spline->bIsFollowing || Spline->CurrentSpline)
|
||||||
|
{
|
||||||
|
return EPS_AI_Behavior_State::Patrol;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return EPS_AI_Behavior_State::Idle;
|
||||||
}
|
}
|
||||||
|
|
||||||
EPS_AI_Behavior_State UPS_AI_Behavior_PersonalityComponent::ApplyReaction()
|
EPS_AI_Behavior_State UPS_AI_Behavior_PersonalityComponent::ApplyReaction()
|
||||||
@ -175,6 +192,103 @@ void UPS_AI_Behavior_PersonalityComponent::HandleStateChanged(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UPS_AI_Behavior_PersonalityComponent::TickComponent(
|
||||||
|
float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
|
||||||
|
{
|
||||||
|
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
|
||||||
|
|
||||||
|
if (bDebug)
|
||||||
|
{
|
||||||
|
DrawDebugInfo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_AI_Behavior_PersonalityComponent::DrawDebugInfo() const
|
||||||
|
{
|
||||||
|
const AActor* Owner = GetOwner();
|
||||||
|
if (!Owner) return;
|
||||||
|
|
||||||
|
const UWorld* World = GetWorld();
|
||||||
|
if (!World) return;
|
||||||
|
|
||||||
|
// Position: above the NPC's head
|
||||||
|
const FVector HeadPos = Owner->GetActorLocation() + FVector(0.f, 0.f, 120.f);
|
||||||
|
|
||||||
|
// ─── Gather info ────────────────────────────────────────────────────
|
||||||
|
const FString NPCName = Owner->GetName();
|
||||||
|
const EPS_AI_Behavior_NPCType NPCType = GetNPCType();
|
||||||
|
|
||||||
|
// TeamId + Hostile from AIController
|
||||||
|
uint8 DebugTeamId = 255;
|
||||||
|
FString ThreatActorName = TEXT("none");
|
||||||
|
float ThreatLevel = PerceivedThreatLevel;
|
||||||
|
bool bHostile = false;
|
||||||
|
|
||||||
|
const APawn* Pawn = Cast<APawn>(Owner);
|
||||||
|
if (Pawn)
|
||||||
|
{
|
||||||
|
if (const APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(Pawn->GetController()))
|
||||||
|
{
|
||||||
|
DebugTeamId = AIC->GetGenericTeamId().GetId();
|
||||||
|
|
||||||
|
// Get ThreatActor from Blackboard
|
||||||
|
if (const UBlackboardComponent* BB = AIC->GetBlackboardComponent())
|
||||||
|
{
|
||||||
|
if (AActor* ThreatActor = Cast<AActor>(BB->GetValueAsObject(PS_AI_Behavior_BB::ThreatActor)))
|
||||||
|
{
|
||||||
|
ThreatActorName = ThreatActor->GetName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Pawn->Implements<UPS_AI_Behavior_Interface>())
|
||||||
|
{
|
||||||
|
bHostile = IPS_AI_Behavior_Interface::Execute_IsBehaviorHostile(const_cast<APawn*>(Pawn));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spline info
|
||||||
|
FString SplineInfo = TEXT("");
|
||||||
|
if (const UPS_AI_Behavior_SplineFollowerComponent* Spline =
|
||||||
|
Owner->FindComponentByClass<UPS_AI_Behavior_SplineFollowerComponent>())
|
||||||
|
{
|
||||||
|
if (Spline->bIsFollowing && Spline->CurrentSpline)
|
||||||
|
{
|
||||||
|
SplineInfo = FString::Printf(TEXT("\nSpline: %s (%.0f%%)"),
|
||||||
|
*Spline->CurrentSpline->GetName(), Spline->GetProgress() * 100.f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Color by state ─────────────────────────────────────────────────
|
||||||
|
FColor TextColor;
|
||||||
|
switch (CurrentState)
|
||||||
|
{
|
||||||
|
case EPS_AI_Behavior_State::Idle: TextColor = FColor::White; break;
|
||||||
|
case EPS_AI_Behavior_State::Patrol: TextColor = FColor::Green; break;
|
||||||
|
case EPS_AI_Behavior_State::Alerted: TextColor = FColor::Yellow; break;
|
||||||
|
case EPS_AI_Behavior_State::Combat: TextColor = FColor::Red; break;
|
||||||
|
case EPS_AI_Behavior_State::Fleeing: TextColor = FColor::Orange; break;
|
||||||
|
case EPS_AI_Behavior_State::TakingCover: TextColor = FColor::Cyan; break;
|
||||||
|
case EPS_AI_Behavior_State::Dead: TextColor = FColor(80, 80, 80); break;
|
||||||
|
default: TextColor = FColor::White; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Build text ─────────────────────────────────────────────────────
|
||||||
|
const FString DebugText = FString::Printf(
|
||||||
|
TEXT("%s [%s] Team:%d %s\n%s Threat:%.2f → %s%s"),
|
||||||
|
*NPCName,
|
||||||
|
*UEnum::GetDisplayValueAsText(NPCType).ToString(),
|
||||||
|
DebugTeamId,
|
||||||
|
bHostile ? TEXT("HOSTILE") : TEXT(""),
|
||||||
|
*UEnum::GetDisplayValueAsText(CurrentState).ToString(),
|
||||||
|
ThreatLevel,
|
||||||
|
*ThreatActorName,
|
||||||
|
*SplineInfo);
|
||||||
|
|
||||||
|
UKismetSystemLibrary::DrawDebugString(
|
||||||
|
GetOwner(), HeadPos, DebugText, nullptr, FLinearColor(TextColor), 0.f);
|
||||||
|
}
|
||||||
|
|
||||||
EPS_AI_Behavior_NPCType UPS_AI_Behavior_PersonalityComponent::GetNPCType() const
|
EPS_AI_Behavior_NPCType UPS_AI_Behavior_PersonalityComponent::GetNPCType() const
|
||||||
{
|
{
|
||||||
// Prefer the IPS_AI_Behavior interface on the owning actor
|
// Prefer the IPS_AI_Behavior interface on the owning actor
|
||||||
|
|||||||
@ -154,141 +154,158 @@ void UPS_AI_Behavior_SplineFollowerComponent::TickComponent(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Advance along spline ───────────────────────────────────────────
|
// ─── Pure Pursuit: project NPC onto spline, target = lookahead ahead ──
|
||||||
// Use the NPC's actual movement speed (from CMC) instead of a fixed speed.
|
// This eliminates gap accumulation in turns. The NPC always steers toward
|
||||||
// This keeps the target point in sync with how fast the character really moves.
|
// a point that follows the curve naturally at a fixed distance ahead.
|
||||||
float Speed = GetEffectiveSpeed();
|
|
||||||
ACharacter* OwnerCharacter = Cast<ACharacter>(GetOwner());
|
|
||||||
if (OwnerCharacter && OwnerCharacter->GetCharacterMovement())
|
|
||||||
{
|
|
||||||
Speed = OwnerCharacter->GetCharacterMovement()->GetMaxSpeed();
|
|
||||||
}
|
|
||||||
const float Delta = Speed * DeltaTime;
|
|
||||||
const float PrevDistance = CurrentDistance;
|
|
||||||
const bool bPrevForward = bMovingForward;
|
|
||||||
|
|
||||||
if (bMovingForward)
|
|
||||||
{
|
|
||||||
CurrentDistance += Delta;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
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 ─────────────────────────────────────────
|
|
||||||
if (CurrentDistance >= SplineLen)
|
|
||||||
{
|
|
||||||
if (CurrentSpline->SplineComp && CurrentSpline->SplineComp->IsClosedLoop())
|
|
||||||
{
|
|
||||||
CurrentDistance = FMath::Fmod(CurrentDistance, SplineLen);
|
|
||||||
}
|
|
||||||
else if (bReverseAtEnd)
|
|
||||||
{
|
|
||||||
// Clamp to the end — don't reverse the target point yet.
|
|
||||||
// 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
|
|
||||||
{
|
|
||||||
CurrentDistance = SplineLen;
|
|
||||||
bIsFollowing = false;
|
|
||||||
OnSplineEndReached.Broadcast(CurrentSpline);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (CurrentDistance <= 0.0f)
|
|
||||||
{
|
|
||||||
if (CurrentSpline->SplineComp && CurrentSpline->SplineComp->IsClosedLoop())
|
|
||||||
{
|
|
||||||
CurrentDistance = SplineLen + CurrentDistance;
|
|
||||||
}
|
|
||||||
else if (bReverseAtEnd)
|
|
||||||
{
|
|
||||||
// Clamp to the start — wait for NPC to catch up
|
|
||||||
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
|
|
||||||
{
|
|
||||||
CurrentDistance = 0.0f;
|
|
||||||
bIsFollowing = false;
|
|
||||||
OnSplineEndReached.Broadcast(CurrentSpline);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Move the pawn ──────────────────────────────────────────────────
|
|
||||||
const FVector TargetLocation = CurrentSpline->GetWorldLocationAtDistance(CurrentDistance);
|
|
||||||
const FRotator TargetRotation = CurrentSpline->GetWorldRotationAtDistance(CurrentDistance);
|
|
||||||
|
|
||||||
AActor* Owner = GetOwner();
|
AActor* Owner = GetOwner();
|
||||||
const FVector CurrentLocation = Owner->GetActorLocation();
|
const FVector CurrentLocation = Owner->GetActorLocation();
|
||||||
const FRotator CurrentRotation = Owner->GetActorRotation();
|
const FRotator CurrentRotation = Owner->GetActorRotation();
|
||||||
|
|
||||||
// Use Character movement if available for proper physics/collision
|
// Project NPC position onto the spline to find where we actually are
|
||||||
ACharacter* Character = Cast<ACharacter>(Owner);
|
float ProjectedDistance = 0.0f;
|
||||||
if (Character && Character->GetCharacterMovement())
|
FVector ProjectedPoint;
|
||||||
{
|
CurrentSpline->GetClosestPointOnSpline(CurrentLocation, ProjectedDistance, ProjectedPoint);
|
||||||
// Steer toward the spline point directly.
|
|
||||||
// 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();
|
|
||||||
|
|
||||||
FVector DesiredVelocity;
|
// Ensure monotonic progress: don't let projection jump backward past current
|
||||||
if (GapToSpline < 1.0f)
|
// (can happen at tight S-curves where two segments are close in world space)
|
||||||
|
const float ProgressDir = bMovingForward ? 1.0f : -1.0f;
|
||||||
|
const float ProgressDelta = (ProjectedDistance - CurrentDistance) * ProgressDir;
|
||||||
|
if (ProgressDelta < -LookaheadDistance)
|
||||||
|
{
|
||||||
|
// Projection jumped too far backward — keep current distance and advance normally
|
||||||
|
float Speed = GetEffectiveSpeed();
|
||||||
|
ACharacter* TmpChar = Cast<ACharacter>(Owner);
|
||||||
|
if (TmpChar && TmpChar->GetCharacterMovement())
|
||||||
{
|
{
|
||||||
// Exactly on the spline — use tangent to avoid zero velocity
|
Speed = TmpChar->GetCharacterMovement()->GetMaxSpeed();
|
||||||
const FVector SplineTangent = CurrentSpline->GetWorldDirectionAtDistance(CurrentDistance)
|
}
|
||||||
* (bMovingForward ? 1.0f : -1.0f);
|
if (bMovingForward)
|
||||||
DesiredVelocity = SplineTangent * Speed;
|
{
|
||||||
|
CurrentDistance += Speed * DeltaTime;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Steer toward the target point on the spline, clamped to Speed
|
CurrentDistance -= Speed * DeltaTime;
|
||||||
DesiredVelocity = ToTarget.GetSafeNormal() * Speed;
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CurrentDistance = ProjectedDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Compute target (lookahead) distance ────────────────────────────
|
||||||
|
float TargetDistance;
|
||||||
|
if (bMovingForward)
|
||||||
|
{
|
||||||
|
TargetDistance = CurrentDistance + LookaheadDistance;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TargetDistance = CurrentDistance - LookaheadDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── End of spline handling ─────────────────────────────────────────
|
||||||
|
if (TargetDistance >= SplineLen)
|
||||||
|
{
|
||||||
|
if (CurrentSpline->SplineComp && CurrentSpline->SplineComp->IsClosedLoop())
|
||||||
|
{
|
||||||
|
TargetDistance = FMath::Fmod(TargetDistance, SplineLen);
|
||||||
|
}
|
||||||
|
else if (bReverseAtEnd)
|
||||||
|
{
|
||||||
|
// Clamp target to end, and reverse when NPC is close enough
|
||||||
|
TargetDistance = SplineLen;
|
||||||
|
const FVector EndPoint = CurrentSpline->GetWorldLocationAtDistance(SplineLen);
|
||||||
|
const float GapToEnd = FVector::Dist2D(CurrentLocation, EndPoint);
|
||||||
|
if (GapToEnd < 80.0f)
|
||||||
|
{
|
||||||
|
bMovingForward = false;
|
||||||
|
TargetDistance = CurrentDistance - LookaheadDistance;
|
||||||
|
TargetDistance = FMath::Max(TargetDistance, 0.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TargetDistance = SplineLen;
|
||||||
|
const FVector EndPoint = CurrentSpline->GetWorldLocationAtDistance(SplineLen);
|
||||||
|
const float GapToEnd = FVector::Dist2D(CurrentLocation, EndPoint);
|
||||||
|
if (GapToEnd < 80.0f)
|
||||||
|
{
|
||||||
|
bIsFollowing = false;
|
||||||
|
OnSplineEndReached.Broadcast(CurrentSpline);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (TargetDistance <= 0.0f)
|
||||||
|
{
|
||||||
|
if (CurrentSpline->SplineComp && CurrentSpline->SplineComp->IsClosedLoop())
|
||||||
|
{
|
||||||
|
TargetDistance = SplineLen + TargetDistance;
|
||||||
|
}
|
||||||
|
else if (bReverseAtEnd)
|
||||||
|
{
|
||||||
|
TargetDistance = 0.0f;
|
||||||
|
const FVector StartPoint = CurrentSpline->GetWorldLocationAtDistance(0.0f);
|
||||||
|
const float GapToStart = FVector::Dist2D(CurrentLocation, StartPoint);
|
||||||
|
if (GapToStart < 80.0f)
|
||||||
|
{
|
||||||
|
bMovingForward = true;
|
||||||
|
TargetDistance = CurrentDistance + LookaheadDistance;
|
||||||
|
TargetDistance = FMath::Min(TargetDistance, SplineLen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TargetDistance = 0.0f;
|
||||||
|
const FVector StartPoint = CurrentSpline->GetWorldLocationAtDistance(0.0f);
|
||||||
|
const float GapToStart = FVector::Dist2D(CurrentLocation, StartPoint);
|
||||||
|
if (GapToStart < 80.0f)
|
||||||
|
{
|
||||||
|
bIsFollowing = false;
|
||||||
|
OnSplineEndReached.Broadcast(CurrentSpline);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Move the pawn ──────────────────────────────────────────────────
|
||||||
|
const FVector TargetLocation = CurrentSpline->GetWorldLocationAtDistance(TargetDistance);
|
||||||
|
|
||||||
|
float Speed = GetEffectiveSpeed();
|
||||||
|
ACharacter* Character = Cast<ACharacter>(Owner);
|
||||||
|
if (Character && Character->GetCharacterMovement())
|
||||||
|
{
|
||||||
|
Speed = Character->GetCharacterMovement()->GetMaxSpeed();
|
||||||
|
|
||||||
|
const FVector ToTarget = TargetLocation - CurrentLocation;
|
||||||
|
const float GapToTarget = ToTarget.Size2D();
|
||||||
|
|
||||||
|
FVector MoveDirection;
|
||||||
|
if (GapToTarget < 1.0f)
|
||||||
|
{
|
||||||
|
// On top of the target — use spline tangent direction
|
||||||
|
MoveDirection = CurrentSpline->GetWorldDirectionAtDistance(CurrentDistance)
|
||||||
|
* (bMovingForward ? 1.0f : -1.0f);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
MoveDirection = ToTarget.GetSafeNormal();
|
||||||
}
|
}
|
||||||
|
|
||||||
Character->GetCharacterMovement()->RequestDirectMove(DesiredVelocity, /*bForceMaxSpeed=*/false);
|
// Use AddMovementInput — works reliably regardless of AIController movement state
|
||||||
|
Character->AddMovementInput(MoveDirection, 1.0f);
|
||||||
|
|
||||||
|
// Debug: log velocity to verify movement is happening
|
||||||
|
const FVector Vel = Character->GetVelocity();
|
||||||
|
if (Vel.Size() < 1.0f)
|
||||||
|
{
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Warning,
|
||||||
|
TEXT("[%s] SplineFollower: AddMovementInput but velocity=%.1f! MaxSpeed=%.0f Gap=%.0f CMC_MovementMode=%d"),
|
||||||
|
*GetOwner()->GetName(), Vel.Size(), Speed, GapToTarget,
|
||||||
|
(int32)Character->GetCharacterMovement()->MovementMode.GetValue());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -296,8 +313,7 @@ void UPS_AI_Behavior_SplineFollowerComponent::TickComponent(
|
|||||||
Owner->SetActorLocation(TargetLocation);
|
Owner->SetActorLocation(TargetLocation);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Smooth rotation — face toward the target point (not the spline tangent)
|
// Smooth rotation — face toward the target point (follows curve naturally)
|
||||||
// This prevents the NPC from looking too far into corners
|
|
||||||
const FVector ToTargetDir = (TargetLocation - CurrentLocation).GetSafeNormal2D();
|
const FVector ToTargetDir = (TargetLocation - CurrentLocation).GetSafeNormal2D();
|
||||||
FRotator FinalTargetRot;
|
FRotator FinalTargetRot;
|
||||||
if (!ToTargetDir.IsNearlyZero())
|
if (!ToTargetDir.IsNearlyZero())
|
||||||
@ -306,8 +322,8 @@ void UPS_AI_Behavior_SplineFollowerComponent::TickComponent(
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Fallback to spline tangent if on top of the target
|
// Fallback to spline tangent
|
||||||
FinalTargetRot = TargetRotation;
|
FinalTargetRot = CurrentSpline->GetWorldRotationAtDistance(CurrentDistance);
|
||||||
if (!bMovingForward)
|
if (!bMovingForward)
|
||||||
{
|
{
|
||||||
FinalTargetRot.Yaw += 180.0f;
|
FinalTargetRot.Yaw += 180.0f;
|
||||||
@ -316,32 +332,36 @@ void UPS_AI_Behavior_SplineFollowerComponent::TickComponent(
|
|||||||
|
|
||||||
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));
|
||||||
|
|
||||||
// ─── Debug drawing ─────────────────────────────────────────────────
|
// ─── Debug drawing ─────────────────────────────────────────────────
|
||||||
if (bDrawDebug)
|
if (bDebug)
|
||||||
{
|
{
|
||||||
const UWorld* World = GetWorld();
|
const UWorld* World = GetWorld();
|
||||||
if (World)
|
if (World)
|
||||||
{
|
{
|
||||||
// Target point on spline (green sphere)
|
// Projected point on spline (small blue sphere = where NPC actually is on spline)
|
||||||
|
DrawDebugSphere(World, ProjectedPoint, 10.0f, 6, FColor::Blue, false, -1.0f, 0, 1.5f);
|
||||||
|
|
||||||
|
// Lookahead target point (green sphere = where NPC steers toward)
|
||||||
DrawDebugSphere(World, TargetLocation, 15.0f, 8, FColor::Green, false, -1.0f, 0, 2.0f);
|
DrawDebugSphere(World, TargetLocation, 15.0f, 8, FColor::Green, false, -1.0f, 0, 2.0f);
|
||||||
|
|
||||||
// Line from NPC to target point (yellow = gap)
|
// Line from NPC to lookahead target (yellow)
|
||||||
DrawDebugLine(World, CurrentLocation, TargetLocation, FColor::Yellow, false, -1.0f, 0, 1.5f);
|
DrawDebugLine(World, CurrentLocation, TargetLocation, FColor::Yellow, false, -1.0f, 0, 1.5f);
|
||||||
|
|
||||||
// Spline tangent direction (cyan arrow)
|
// Spline tangent direction at NPC position (cyan arrow)
|
||||||
const FVector Tangent = CurrentSpline->GetWorldDirectionAtDistance(CurrentDistance)
|
const FVector Tangent = CurrentSpline->GetWorldDirectionAtDistance(CurrentDistance)
|
||||||
* (bMovingForward ? 1.0f : -1.0f);
|
* (bMovingForward ? 1.0f : -1.0f);
|
||||||
DrawDebugDirectionalArrow(World, TargetLocation, TargetLocation + Tangent * 150.0f,
|
DrawDebugDirectionalArrow(World, ProjectedPoint, ProjectedPoint + Tangent * 150.0f,
|
||||||
20.0f, FColor::Cyan, false, -1.0f, 0, 2.0f);
|
20.0f, FColor::Cyan, false, -1.0f, 0, 2.0f);
|
||||||
|
|
||||||
// Gap distance text
|
// Info text
|
||||||
const float DebugGap = FVector::Dist(CurrentLocation, TargetLocation);
|
const float DebugGap = FVector::Dist2D(CurrentLocation, TargetLocation);
|
||||||
|
const float SplineGap = FVector::Dist2D(CurrentLocation, ProjectedPoint);
|
||||||
DrawDebugString(World, CurrentLocation + FVector(0, 0, 100.0f),
|
DrawDebugString(World, CurrentLocation + FVector(0, 0, 100.0f),
|
||||||
FString::Printf(TEXT("Gap: %.0fcm Dist: %.0f/%.0f"),
|
FString::Printf(TEXT("Look: %.0fcm Off: %.0fcm D: %.0f/%.0f"),
|
||||||
DebugGap, CurrentDistance, CurrentSpline->GetSplineLength()),
|
DebugGap, SplineGap, CurrentDistance, SplineLen),
|
||||||
nullptr, FColor::White, 0.0f, true);
|
nullptr, FColor::White, -1.0f, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -382,7 +402,7 @@ void UPS_AI_Behavior_SplineFollowerComponent::HandleJunctions()
|
|||||||
LastHandledJunctionIndex = i;
|
LastHandledJunctionIndex = i;
|
||||||
OnApproachingJunction.Broadcast(CurrentSpline, i, DistToJunction);
|
OnApproachingJunction.Broadcast(CurrentSpline, i, DistToJunction);
|
||||||
|
|
||||||
if (bDrawDebug)
|
if (bDebug)
|
||||||
{
|
{
|
||||||
UE_LOG(LogPS_AI_Behavior, Log,
|
UE_LOG(LogPS_AI_Behavior, Log,
|
||||||
TEXT("[%s] Junction detected: idx=%d, dist along=%.0f, dist to=%.0fcm, worldPos=(%.0f,%.0f,%.0f)"),
|
TEXT("[%s] Junction detected: idx=%d, dist along=%.0f, dist to=%.0fcm, worldPos=(%.0f,%.0f,%.0f)"),
|
||||||
@ -398,7 +418,7 @@ void UPS_AI_Behavior_SplineFollowerComponent::HandleJunctions()
|
|||||||
const float Roll = FMath::FRand();
|
const float Roll = FMath::FRand();
|
||||||
if (Roll > SwitchChance)
|
if (Roll > SwitchChance)
|
||||||
{
|
{
|
||||||
if (bDrawDebug)
|
if (bDebug)
|
||||||
{
|
{
|
||||||
UE_LOG(LogPS_AI_Behavior, Log,
|
UE_LOG(LogPS_AI_Behavior, Log,
|
||||||
TEXT("[%s] Junction %d: skipped (roll=%.2f > chance=%.2f)"),
|
TEXT("[%s] Junction %d: skipped (roll=%.2f > chance=%.2f)"),
|
||||||
@ -435,7 +455,7 @@ void UPS_AI_Behavior_SplineFollowerComponent::HandleJunctions()
|
|||||||
const float WorldGap = FVector::Dist(CurrentPos, JunctionPos);
|
const float WorldGap = FVector::Dist(CurrentPos, JunctionPos);
|
||||||
if (WorldGap > JunctionDetectionDistance * 2.0f)
|
if (WorldGap > JunctionDetectionDistance * 2.0f)
|
||||||
{
|
{
|
||||||
if (bDrawDebug)
|
if (bDebug)
|
||||||
{
|
{
|
||||||
UE_LOG(LogPS_AI_Behavior, Log,
|
UE_LOG(LogPS_AI_Behavior, Log,
|
||||||
TEXT("[%s] Junction %d: rejected (worldGap=%.0f > detect=%.0f)"),
|
TEXT("[%s] Junction %d: rejected (worldGap=%.0f > detect=%.0f)"),
|
||||||
@ -474,7 +494,7 @@ void UPS_AI_Behavior_SplineFollowerComponent::HandleJunctions()
|
|||||||
// This prevents full U-turns but allows wide turns at crossings
|
// This prevents full U-turns but allows wide turns at crossings
|
||||||
if (BestDot < -0.7f)
|
if (BestDot < -0.7f)
|
||||||
{
|
{
|
||||||
if (bDrawDebug)
|
if (bDebug)
|
||||||
{
|
{
|
||||||
UE_LOG(LogPS_AI_Behavior, Log,
|
UE_LOG(LogPS_AI_Behavior, Log,
|
||||||
TEXT("[%s] Junction %d: rejected U-turn (dot=%.2f)"),
|
TEXT("[%s] Junction %d: rejected U-turn (dot=%.2f)"),
|
||||||
|
|||||||
@ -19,6 +19,12 @@ void APS_AI_Behavior_SplinePath::BeginPlay()
|
|||||||
{
|
{
|
||||||
Super::BeginPlay();
|
Super::BeginPlay();
|
||||||
UpdateSplineVisualization();
|
UpdateSplineVisualization();
|
||||||
|
|
||||||
|
// Hide spline debug at runtime — only visible in editor
|
||||||
|
if (SplineComp)
|
||||||
|
{
|
||||||
|
SplineComp->SetDrawDebug(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool APS_AI_Behavior_SplinePath::IsAccessibleTo(EPS_AI_Behavior_NPCType NPCType) const
|
bool APS_AI_Behavior_SplinePath::IsAccessibleTo(EPS_AI_Behavior_NPCType NPCType) const
|
||||||
|
|||||||
@ -0,0 +1,19 @@
|
|||||||
|
// Copyright Asterion. All Rights Reserved.
|
||||||
|
|
||||||
|
#include "PS_AI_Behavior_TeamComponent.h"
|
||||||
|
|
||||||
|
UPS_AI_Behavior_TeamComponent::UPS_AI_Behavior_TeamComponent()
|
||||||
|
{
|
||||||
|
PrimaryComponentTick.bCanEverTick = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8 UPS_AI_Behavior_TeamComponent::GetTeamId() const
|
||||||
|
{
|
||||||
|
switch (Role)
|
||||||
|
{
|
||||||
|
case EPS_AI_Behavior_NPCType::Civilian: return 1;
|
||||||
|
case EPS_AI_Behavior_NPCType::Enemy: return 2;
|
||||||
|
case EPS_AI_Behavior_NPCType::Protector: return 3;
|
||||||
|
default: return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -32,6 +32,7 @@ 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;
|
||||||
virtual EBTNodeResult::Type AbortTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
|
virtual EBTNodeResult::Type AbortTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
|
||||||
|
virtual void OnTaskFinished(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, EBTNodeResult::Type TaskResult) override;
|
||||||
virtual FString GetStaticDescription() const override;
|
virtual FString GetStaticDescription() const override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@ -101,6 +101,21 @@ public:
|
|||||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS AI Behavior")
|
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS AI Behavior")
|
||||||
void OnBehaviorStateChanged(EPS_AI_Behavior_State NewState, EPS_AI_Behavior_State OldState);
|
void OnBehaviorStateChanged(EPS_AI_Behavior_State NewState, EPS_AI_Behavior_State OldState);
|
||||||
|
|
||||||
|
// ─── Target Validation ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a perceived actor is still a valid target (alive, not despawning, etc.).
|
||||||
|
* Called by the perception and combat systems to discard dead or invalid targets.
|
||||||
|
*
|
||||||
|
* The host project implements this to check its own health/death system.
|
||||||
|
* Default returns true (all actors are valid unless the Pawn says otherwise).
|
||||||
|
*
|
||||||
|
* @param TargetActor The actor to validate.
|
||||||
|
* @return True if the actor can still be targeted.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS AI Behavior")
|
||||||
|
bool IsTargetActorValid(AActor* TargetActor) const;
|
||||||
|
|
||||||
// ─── Combat ─────────────────────────────────────────────────────────
|
// ─── Combat ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -36,6 +36,15 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Personality")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Personality")
|
||||||
TObjectPtr<UPS_AI_Behavior_PersonalityProfile> Profile;
|
TObjectPtr<UPS_AI_Behavior_PersonalityProfile> Profile;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Master debug toggle for this NPC.
|
||||||
|
* When enabled, draws floating text above the NPC's head with:
|
||||||
|
* Name, NPCType, TeamId, State, ThreatLevel, ThreatActor, Hostile, Spline.
|
||||||
|
* Also enables debug visuals on SplineFollowerComponent.
|
||||||
|
*/
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Personality|Debug")
|
||||||
|
bool bDebug = false;
|
||||||
|
|
||||||
// ─── Runtime State ──────────────────────────────────────────────────
|
// ─── Runtime State ──────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -106,6 +115,8 @@ public:
|
|||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual void BeginPlay() override;
|
virtual void BeginPlay() override;
|
||||||
|
virtual void TickComponent(float DeltaTime, ELevelTick TickType,
|
||||||
|
FActorComponentTickFunction* ThisTickFunction) override;
|
||||||
|
|
||||||
UFUNCTION()
|
UFUNCTION()
|
||||||
void OnRep_CurrentState(EPS_AI_Behavior_State OldState);
|
void OnRep_CurrentState(EPS_AI_Behavior_State OldState);
|
||||||
@ -118,4 +129,7 @@ private:
|
|||||||
* - Calls IPS_AI_Behavior::OnBehaviorStateChanged on the Pawn
|
* - Calls IPS_AI_Behavior::OnBehaviorStateChanged on the Pawn
|
||||||
*/
|
*/
|
||||||
void HandleStateChanged(EPS_AI_Behavior_State OldState, EPS_AI_Behavior_State NewState);
|
void HandleStateChanged(EPS_AI_Behavior_State OldState, EPS_AI_Behavior_State NewState);
|
||||||
|
|
||||||
|
/** Draw floating debug text above the NPC's head. */
|
||||||
|
void DrawDebugInfo() const;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -58,6 +58,14 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spline Follower", meta = (ClampMin = "30.0"))
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spline Follower", meta = (ClampMin = "30.0"))
|
||||||
float RotationInterpSpeed = 360.0f;
|
float RotationInterpSpeed = 360.0f;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How far ahead on the spline to place the target point (cm).
|
||||||
|
* Larger = smoother wider turns, smaller = tighter cornering.
|
||||||
|
* The NPC always steers toward this point, which follows the spline curve naturally.
|
||||||
|
*/
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spline Follower", meta = (ClampMin = "50.0"))
|
||||||
|
float LookaheadDistance = 200.0f;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether to auto-choose a spline at junctions.
|
* Whether to auto-choose a spline at junctions.
|
||||||
* If false, OnApproachingJunction fires and you must call SwitchToSpline manually.
|
* If false, OnApproachingJunction fires and you must call SwitchToSpline manually.
|
||||||
@ -75,7 +83,7 @@ public:
|
|||||||
|
|
||||||
/** Draw debug info: target point on spline, direction, gap distance. */
|
/** Draw debug info: target point on spline, direction, gap distance. */
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spline Follower|Debug")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spline Follower|Debug")
|
||||||
bool bDrawDebug = false;
|
bool bDebug = false;
|
||||||
|
|
||||||
// ─── Runtime State ──────────────────────────────────────────────────
|
// ─── Runtime State ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,46 @@
|
|||||||
|
// Copyright Asterion. All Rights Reserved.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Components/ActorComponent.h"
|
||||||
|
#include "GenericTeamAgentInterface.h"
|
||||||
|
#include "PS_AI_Behavior_Definitions.h"
|
||||||
|
#include "PS_AI_Behavior_TeamComponent.generated.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drop this component on any Pawn (player or non-AI character) to give it
|
||||||
|
* a role that the PS AI Behavior perception system will recognize.
|
||||||
|
*
|
||||||
|
* NPCs controlled by PS_AI_Behavior_AIController already have their role
|
||||||
|
* set automatically via the Interface — this component is meant for Pawns
|
||||||
|
* WITHOUT that controller (typically the player character).
|
||||||
|
*
|
||||||
|
* Role determines team affiliation:
|
||||||
|
* Civilian (Team 1) — Enemies attack, Protectors defend
|
||||||
|
* Enemy (Team 2) — Protectors and Civilians react
|
||||||
|
* Protector (Team 3) — Allied with Civilians, hostile to Enemies
|
||||||
|
*/
|
||||||
|
UCLASS(ClassGroup = "PS AI Behavior", meta = (BlueprintSpawnableComponent,
|
||||||
|
DisplayName = "PS AI Behavior Pawn Identity"))
|
||||||
|
class PS_AI_BEHAVIOR_API UPS_AI_Behavior_TeamComponent : public UActorComponent
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPS_AI_Behavior_TeamComponent();
|
||||||
|
|
||||||
|
/** Role of this actor in the behavior system. Determines team affiliation. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS AI Behavior|Team")
|
||||||
|
EPS_AI_Behavior_NPCType Role = EPS_AI_Behavior_NPCType::Civilian;
|
||||||
|
|
||||||
|
/** Change role at runtime. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Team")
|
||||||
|
void SetRole(EPS_AI_Behavior_NPCType NewRole) { Role = NewRole; }
|
||||||
|
|
||||||
|
/** Returns the TeamId derived from the current Role. */
|
||||||
|
uint8 GetTeamId() const;
|
||||||
|
|
||||||
|
/** Returns the TeamId as FGenericTeamId. */
|
||||||
|
FGenericTeamId GetGenericTeamId() const { return FGenericTeamId(GetTeamId()); }
|
||||||
|
};
|
||||||
Loading…
x
Reference in New Issue
Block a user