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:
parent
9d054cc46f
commit
1799ba28c0
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
#include "BT/PS_AI_Behavior_BTTask_Attack.h"
|
#include "BT/PS_AI_Behavior_BTTask_Attack.h"
|
||||||
#include "PS_AI_Behavior_AIController.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 "PS_AI_Behavior_Definitions.h"
|
||||||
#include "BehaviorTree/BlackboardComponent.h"
|
#include "BehaviorTree/BlackboardComponent.h"
|
||||||
#include "Navigation/PathFollowingComponent.h"
|
#include "Navigation/PathFollowingComponent.h"
|
||||||
@ -23,7 +23,6 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_Attack::ExecuteTask(
|
|||||||
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
|
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
|
||||||
if (!BB) return EBTNodeResult::Failed;
|
if (!BB) return EBTNodeResult::Failed;
|
||||||
|
|
||||||
// Get threat actor
|
|
||||||
AActor* Target = Cast<AActor>(BB->GetValueAsObject(PS_AI_Behavior_BB::ThreatActor));
|
AActor* Target = Cast<AActor>(BB->GetValueAsObject(PS_AI_Behavior_BB::ThreatActor));
|
||||||
if (!Target)
|
if (!Target)
|
||||||
{
|
{
|
||||||
@ -31,46 +30,31 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_Attack::ExecuteTask(
|
|||||||
return EBTNodeResult::Failed;
|
return EBTNodeResult::Failed;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get combat component
|
APawn* Pawn = AIC->GetPawn();
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
FAttackMemory* Memory = reinterpret_cast<FAttackMemory*>(NodeMemory);
|
FAttackMemory* Memory = reinterpret_cast<FAttackMemory*>(NodeMemory);
|
||||||
Memory->bMovingToTarget = true;
|
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;
|
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;
|
AActor* Target = BB ? Cast<AActor>(BB->GetValueAsObject(PS_AI_Behavior_BB::ThreatActor)) : nullptr;
|
||||||
if (!Target)
|
if (!Target)
|
||||||
{
|
{
|
||||||
AIC->StopMovement();
|
|
||||||
FinishLatentTask(OwnerComp, EBTNodeResult::Failed);
|
FinishLatentTask(OwnerComp, EBTNodeResult::Failed);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
UPS_AI_Behavior_CombatComponent* Combat =
|
FAttackMemory* Memory = reinterpret_cast<FAttackMemory*>(NodeMemory);
|
||||||
AIC->GetPawn()->FindComponentByClass<UPS_AI_Behavior_CombatComponent>();
|
|
||||||
if (!Combat)
|
// Keep moving toward target if out of range
|
||||||
|
if (Memory->bMovingToTarget && AIC->GetMoveStatus() == EPathFollowingStatus::Idle)
|
||||||
{
|
{
|
||||||
FinishLatentTask(OwnerComp, EBTNodeResult::Failed);
|
// Re-issue move if target moved
|
||||||
return;
|
AIC->MoveToActor(Target, AttackMoveRadius, /*bUsePathfinding=*/true, /*bAllowStrafe=*/true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we can attack now
|
// The Pawn handles the actual shooting/melee via the interface
|
||||||
if (Combat->IsInAttackRange(Target))
|
// We just keep the NPC moving toward the 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);
|
|
||||||
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);
|
|
||||||
|
|
||||||
if (Result == EPathFollowingRequestResult::Failed)
|
|
||||||
{
|
|
||||||
FinishLatentTask(OwnerComp, EBTNodeResult::Failed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
EBTNodeResult::Type UPS_AI_Behavior_BTTask_Attack::AbortTask(
|
EBTNodeResult::Type UPS_AI_Behavior_BTTask_Attack::AbortTask(
|
||||||
@ -144,11 +96,18 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_Attack::AbortTask(
|
|||||||
if (AIC)
|
if (AIC)
|
||||||
{
|
{
|
||||||
AIC->StopMovement();
|
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;
|
return EBTNodeResult::Aborted;
|
||||||
}
|
}
|
||||||
|
|
||||||
FString UPS_AI_Behavior_BTTask_Attack::GetStaticDescription() const
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,9 +7,14 @@
|
|||||||
#include "PS_AI_Behavior_BTTask_Attack.generated.h"
|
#include "PS_AI_Behavior_BTTask_Attack.generated.h"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* BT Task: Move to and attack the threat actor.
|
* BT Task: Move toward the threat actor and delegate combat to the Pawn.
|
||||||
* 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.
|
* 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"))
|
UCLASS(meta = (DisplayName = "PS AI: Attack"))
|
||||||
class PS_AI_BEHAVIOR_API UPS_AI_Behavior_BTTask_Attack : public UBTTaskNode
|
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:
|
public:
|
||||||
UPS_AI_Behavior_BTTask_Attack();
|
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:
|
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;
|
||||||
@ -29,6 +38,7 @@ private:
|
|||||||
struct FAttackMemory
|
struct FAttackMemory
|
||||||
{
|
{
|
||||||
bool bMovingToTarget = false;
|
bool bMovingToTarget = false;
|
||||||
|
bool bAttacking = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
virtual uint16 GetInstanceMemorySize() const override { return sizeof(FAttackMemory); }
|
virtual uint16 GetInstanceMemorySize() const override { return sizeof(FAttackMemory); }
|
||||||
|
|||||||
@ -100,4 +100,30 @@ 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);
|
||||||
|
|
||||||
|
// ─── 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;
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user