Delegate combat to Pawn via IPS_AI_Behavior interface

- Add BehaviorStartAttack/BehaviorStopAttack to IPS_AI_Behavior_Interface
- Attack task now calls interface instead of CombatComponent directly
- Task stays InProgress permanently, Decorator Observer Aborts handles exit
- Remove CombatComponent dependency from Attack task
- Pawn handles actual aiming/shooting via its own systems

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
j.foucher 2026-03-26 18:42:30 +01:00
parent 9d054cc46f
commit 1799ba28c0
3 changed files with 80 additions and 85 deletions

View File

@ -2,7 +2,7 @@
#include "BT/PS_AI_Behavior_BTTask_Attack.h"
#include "PS_AI_Behavior_AIController.h"
#include "PS_AI_Behavior_CombatComponent.h"
#include "PS_AI_Behavior_Interface.h"
#include "PS_AI_Behavior_Definitions.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "Navigation/PathFollowingComponent.h"
@ -23,7 +23,6 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_Attack::ExecuteTask(
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
if (!BB) return EBTNodeResult::Failed;
// Get threat actor
AActor* Target = Cast<AActor>(BB->GetValueAsObject(PS_AI_Behavior_BB::ThreatActor));
if (!Target)
{
@ -31,46 +30,31 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_Attack::ExecuteTask(
return EBTNodeResult::Failed;
}
// Get combat component
UPS_AI_Behavior_CombatComponent* Combat =
AIC->GetPawn()->FindComponentByClass<UPS_AI_Behavior_CombatComponent>();
if (!Combat)
{
UE_LOG(LogPS_AI_Behavior, Warning,
TEXT("[%s] Attack: no CombatComponent on Pawn."), *AIC->GetName());
return EBTNodeResult::Failed;
}
UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[%s] Attack: target='%s', range=%.0f, dist=%.0f, inRange=%d, canAttack=%d"),
*AIC->GetName(), *Target->GetName(), Combat->AttackRange,
FVector::Dist(AIC->GetPawn()->GetActorLocation(), Target->GetActorLocation()),
(int32)Combat->IsInAttackRange(Target), (int32)Combat->CanAttack());
// Try to attack immediately if in range
if (Combat->IsInAttackRange(Target))
{
if (Combat->CanAttack())
{
Combat->ExecuteAttack(Target);
}
// Stay InProgress — keep attacking while in combat state
return EBTNodeResult::InProgress;
}
// Out of range — move toward target
const EPathFollowingRequestResult::Type Result = AIC->MoveToActor(
Target, Combat->AttackRange * 0.8f, /*bUsePathfinding=*/true,
/*bAllowStrafe=*/true);
if (Result == EPathFollowingRequestResult::Failed)
{
UE_LOG(LogPS_AI_Behavior, Warning, TEXT("[%s] Attack: MoveToActor failed — no path to target."), *AIC->GetName());
// Stay InProgress anyway — will retry next tick instead of giving up
return EBTNodeResult::InProgress;
}
APawn* Pawn = AIC->GetPawn();
FAttackMemory* Memory = reinterpret_cast<FAttackMemory*>(NodeMemory);
Memory->bMovingToTarget = false;
Memory->bAttacking = false;
// Tell the Pawn to start attacking via interface
if (Pawn->Implements<UPS_AI_Behavior_Interface>())
{
IPS_AI_Behavior_Interface::Execute_BehaviorStartAttack(Pawn, Target);
Memory->bAttacking = true;
}
// Move toward target
const EPathFollowingRequestResult::Type Result = AIC->MoveToActor(
Target, AttackMoveRadius, /*bUsePathfinding=*/true, /*bAllowStrafe=*/true);
if (Result != EPathFollowingRequestResult::AlreadyAtGoal)
{
Memory->bMovingToTarget = true;
}
UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[%s] Attack: started on '%s'"),
*AIC->GetName(), *Target->GetName());
// Stay InProgress — the Decorator Observer Aborts will pull us out
return EBTNodeResult::InProgress;
}
@ -88,53 +72,21 @@ void UPS_AI_Behavior_BTTask_Attack::TickTask(
AActor* Target = BB ? Cast<AActor>(BB->GetValueAsObject(PS_AI_Behavior_BB::ThreatActor)) : nullptr;
if (!Target)
{
AIC->StopMovement();
FinishLatentTask(OwnerComp, EBTNodeResult::Failed);
return;
}
UPS_AI_Behavior_CombatComponent* Combat =
AIC->GetPawn()->FindComponentByClass<UPS_AI_Behavior_CombatComponent>();
if (!Combat)
{
FinishLatentTask(OwnerComp, EBTNodeResult::Failed);
return;
}
// Check if we can attack now
if (Combat->IsInAttackRange(Target))
{
FAttackMemory* Memory = reinterpret_cast<FAttackMemory*>(NodeMemory);
if (Memory->bMovingToTarget)
{
AIC->StopMovement();
Memory->bMovingToTarget = false;
}
if (Combat->CanAttack())
{
Combat->ExecuteAttack(Target);
}
// Stay InProgress — keep attacking (cooldown handles the rate)
// Observer Aborts on the Decorator will pull us out when state changes
}
else
{
// Still moving — check if movement failed
FAttackMemory* Memory = reinterpret_cast<FAttackMemory*>(NodeMemory);
// Keep moving toward target if out of range
if (Memory->bMovingToTarget && AIC->GetMoveStatus() == EPathFollowingStatus::Idle)
{
// Movement ended but not in range — try again
const EPathFollowingRequestResult::Type Result = AIC->MoveToActor(
Target, Combat->AttackRange * 0.8f, /*bUsePathfinding=*/true,
/*bAllowStrafe=*/true);
// Re-issue move if target moved
AIC->MoveToActor(Target, AttackMoveRadius, /*bUsePathfinding=*/true, /*bAllowStrafe=*/true);
}
if (Result == EPathFollowingRequestResult::Failed)
{
FinishLatentTask(OwnerComp, EBTNodeResult::Failed);
}
}
}
// The Pawn handles the actual shooting/melee via the interface
// We just keep the NPC moving toward the target
}
EBTNodeResult::Type UPS_AI_Behavior_BTTask_Attack::AbortTask(
@ -144,11 +96,18 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_Attack::AbortTask(
if (AIC)
{
AIC->StopMovement();
// Tell the Pawn to stop attacking
APawn* Pawn = AIC->GetPawn();
if (Pawn && Pawn->Implements<UPS_AI_Behavior_Interface>())
{
IPS_AI_Behavior_Interface::Execute_BehaviorStopAttack(Pawn);
}
}
return EBTNodeResult::Aborted;
}
FString UPS_AI_Behavior_BTTask_Attack::GetStaticDescription() const
{
return TEXT("Move to threat and attack via CombatComponent.");
return FString::Printf(TEXT("Move to threat (radius %.0fcm) and attack via interface."), AttackMoveRadius);
}

View File

@ -7,9 +7,14 @@
#include "PS_AI_Behavior_BTTask_Attack.generated.h"
/**
* BT Task: Move to and attack the threat actor.
* If out of range, moves toward the target. If in range, executes attack via CombatComponent.
* Succeeds after one attack, fails if target is lost or unreachable.
* BT Task: Move toward the threat actor and delegate combat to the Pawn.
*
* Calls IPS_AI_Behavior_Interface::BehaviorStartAttack() on enter and
* BehaviorStopAttack() on abort. The Pawn handles the actual combat
* (aim, fire, melee, etc.) via its own systems.
*
* Stays InProgress permanently the Decorator Observer Aborts pulls it out
* when the BehaviorState changes away from Combat.
*/
UCLASS(meta = (DisplayName = "PS AI: Attack"))
class PS_AI_BEHAVIOR_API UPS_AI_Behavior_BTTask_Attack : public UBTTaskNode
@ -19,6 +24,10 @@ class PS_AI_BEHAVIOR_API UPS_AI_Behavior_BTTask_Attack : public UBTTaskNode
public:
UPS_AI_Behavior_BTTask_Attack();
/** How close the NPC tries to get to the target (cm). */
UPROPERTY(EditAnywhere, Category = "Attack", meta = (ClampMin = "50.0"))
float AttackMoveRadius = 300.0f;
protected:
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
@ -29,6 +38,7 @@ private:
struct FAttackMemory
{
bool bMovingToTarget = false;
bool bAttacking = false;
};
virtual uint16 GetInstanceMemorySize() const override { return sizeof(FAttackMemory); }

View File

@ -100,4 +100,30 @@ public:
*/
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS AI Behavior")
void OnBehaviorStateChanged(EPS_AI_Behavior_State NewState, EPS_AI_Behavior_State OldState);
// ─── Combat ─────────────────────────────────────────────────────────
/**
* Order the Pawn to start attacking a target.
* The Pawn implements this with its own combat system (aim, fire, melee, etc.).
* Called when the BT enters the Attack task.
*
* @param Target The actor to attack.
*/
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS AI Behavior")
void BehaviorStartAttack(AActor* Target);
/**
* Order the Pawn to stop attacking.
* Called when the BT exits the Attack task (state changed, target lost, etc.).
*/
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS AI Behavior")
void BehaviorStopAttack();
/**
* Query whether the Pawn can currently attack the target (has ammo, weapon ready, etc.).
* If false, the BT will keep the NPC in combat stance but won't call BehaviorStartAttack.
*/
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS AI Behavior")
bool CanBehaviorAttack(AActor* Target) const;
};