Fix PS_AI_Behavior compilation errors for UE 5.5

- Remove NavigationSystem from .uplugin Plugins (it's a module, not a plugin)
- Fix UInterface naming: IPS_AI_Behavior -> IPS_AI_Behavior_Interface (UHT requirement)
- Fix TWeakObjectPtr<AActor> TArray not Blueprint-compatible (remove BlueprintReadOnly)
- Fix UseBlackboard TObjectPtr ref: use raw pointer intermediary
- Fix FEdMode::HandleClick signature: FInputClick -> FViewportClick (UE 5.5)
- Fix SplineNetwork: use OnWorldBeginPlay(UWorld&) override instead of delegate
- Fix PerceptionComponent: remove const from methods calling non-const GetActorsPerception
- Fix EQS SetScore: use 5-arg float overload (Score, FilterMin, FilterMax)
- Fix BTTask_FindCover: add missing Definitions.h include, fix const World
- Fix BTTask_FollowSpline: replace AddWeakLambda with polling in TickTask
- Fix CoverPoint: initialize Color before switch, add default case
- Export LogPS_AI_Behavior with PS_AI_BEHAVIOR_API for cross-module visibility
- Remove unused variables (WorldOrigin, WorldDirection)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
j.foucher 2026-03-26 08:41:42 +01:00
parent 5d5b85380a
commit 909583a1bb
20 changed files with 564 additions and 76 deletions

View File

@ -0,0 +1,511 @@
# PS_AI_Behavior — Plan d'implémentation V1
## Vue d'ensemble
Plugin UE5.5 pour gérer les comportements de NPCs (civils et ennemis) via Behavior Trees, EQS et un système de personnalité à scores. Navigation sur NavMesh, détection d'ennemis, combat basique, fuite, couverture.
Dépendance optionnelle vers PS_AI_ConvAgent (détectée à l'exécution, pas de link-time dependency).
---
## 1. Structure du plugin
```
Plugins/PS_AI_Behavior/
├── PS_AI_Behavior.uplugin
├── Config/
│ └── DefaultPS_AI_Behavior.ini
├── Content/
│ ├── BehaviorTrees/
│ │ ├── BT_Civilian.uasset (BT civils)
│ │ └── BT_Enemy.uasset (BT ennemis)
│ ├── EQS/
│ │ ├── EQS_FindCover.uasset (trouver couverture)
│ │ ├── EQS_FindFleePoint.uasset (point de fuite)
│ │ └── EQS_FindPatrolPoint.uasset (point de patrouille)
│ └── Data/
│ ├── DA_Trait_Coward.uasset (exemple Data Asset)
│ └── DA_Trait_Aggressive.uasset
└── Source/
├── PS_AI_Behavior/ (module Runtime)
│ ├── PS_AI_Behavior.Build.cs
│ ├── Public/
│ │ ├── PS_AI_Behavior.h (module def)
│ │ ├── PS_AI_Behavior_Definitions.h (enums, structs, log category)
│ │ ├── PS_AI_Behavior_Settings.h (Project Settings)
│ │ │
│ │ ├── PS_AI_Behavior_AIController.h (AIController principal)
│ │ ├── PS_AI_Behavior_PersonalityComponent.h (traits de personnalité)
│ │ ├── PS_AI_Behavior_PerceptionComponent.h (wrapper AIPerception)
│ │ ├── PS_AI_Behavior_CombatComponent.h (état combat)
│ │ ├── PS_AI_Behavior_PersonalityProfile.h (Data Asset profil)
│ │ │
│ │ ├── BT/ (BT Tasks, Services, Decorators)
│ │ │ ├── PS_AI_Behavior_BTTask_FindCover.h
│ │ │ ├── PS_AI_Behavior_BTTask_FleeFrom.h
│ │ │ ├── PS_AI_Behavior_BTTask_Attack.h
│ │ │ ├── PS_AI_Behavior_BTTask_Patrol.h
│ │ │ ├── PS_AI_Behavior_BTService_UpdateThreat.h
│ │ │ ├── PS_AI_Behavior_BTService_EvaluateReaction.h
│ │ │ └── PS_AI_Behavior_BTDecorator_CheckTrait.h
│ │ │
│ │ └── EQS/
│ │ ├── PS_AI_Behavior_EQSContext_Threat.h
│ │ └── PS_AI_Behavior_EQSTest_CoverQuality.h
│ │
│ └── Private/
│ ├── PS_AI_Behavior.cpp
│ ├── PS_AI_Behavior_Settings.cpp
│ ├── PS_AI_Behavior_AIController.cpp
│ ├── PS_AI_Behavior_PersonalityComponent.cpp
│ ├── PS_AI_Behavior_PerceptionComponent.cpp
│ ├── PS_AI_Behavior_CombatComponent.cpp
│ ├── PS_AI_Behavior_PersonalityProfile.cpp
│ ├── BT/
│ │ ├── PS_AI_Behavior_BTTask_FindCover.cpp
│ │ ├── PS_AI_Behavior_BTTask_FleeFrom.cpp
│ │ ├── PS_AI_Behavior_BTTask_Attack.cpp
│ │ ├── PS_AI_Behavior_BTTask_Patrol.cpp
│ │ ├── PS_AI_Behavior_BTService_UpdateThreat.cpp
│ │ ├── PS_AI_Behavior_BTService_EvaluateReaction.cpp
│ │ └── PS_AI_Behavior_BTDecorator_CheckTrait.cpp
│ └── EQS/
│ ├── PS_AI_Behavior_EQSContext_Threat.cpp
│ └── PS_AI_Behavior_EQSTest_CoverQuality.cpp
└── PS_AI_BehaviorEditor/ (module Editor — futur, pas V1)
├── PS_AI_BehaviorEditor.Build.cs
└── ...
```
---
## 2. Classes principales
### 2.1 Definitions (`PS_AI_Behavior_Definitions.h`)
```cpp
// Log category
DECLARE_LOG_CATEGORY_EXTERN(LogPS_AI_Behavior, Log, All);
// Type de NPC
UENUM(BlueprintType)
enum class EPS_AI_Behavior_NPCType : uint8
{
Civilian,
Enemy,
Neutral
};
// État comportemental haut-niveau
UENUM(BlueprintType)
enum class EPS_AI_Behavior_State : uint8
{
Idle,
Patrol,
Alerted,
Combat,
Fleeing,
TakingCover,
Dead
};
// Axes de personnalité (scores 0.0 → 1.0)
UENUM(BlueprintType)
enum class EPS_AI_Behavior_TraitAxis : uint8
{
Courage, // 0 = lâche, 1 = téméraire
Aggressivity, // 0 = pacifique, 1 = violent
Loyalty, // 0 = égoïste, 1 = dévoué
Caution, // 0 = imprudent, 1 = prudent
Discipline // 0 = indiscipliné, 1 = discipliné
};
// Struct pour un trait + valeur
USTRUCT(BlueprintType)
struct FPS_AI_Behavior_TraitScore
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta=(ClampMin=0.0, ClampMax=1.0))
float Value = 0.5f;
};
```
### 2.2 PersonalityProfile — Data Asset (`PS_AI_Behavior_PersonalityProfile.h`)
```cpp
UCLASS(BlueprintType)
class PS_AI_BEHAVIOR_API UPS_AI_Behavior_PersonalityProfile : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Personality")
FText ProfileName;
// Scores par axe : TMap<EPS_AI_Behavior_TraitAxis, float>
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Personality")
TMap<EPS_AI_Behavior_TraitAxis, float> TraitScores;
// Seuils de réaction
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Reaction Thresholds",
meta=(ClampMin=0.0, ClampMax=1.0))
float FleeThreshold = 0.6f; // Threat level au-delà duquel on fuit (modulé par Courage)
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Reaction Thresholds",
meta=(ClampMin=0.0, ClampMax=1.0))
float AttackThreshold = 0.4f; // Threat level au-delà duquel on attaque (modulé par Aggressivity)
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Behavior")
EPS_AI_Behavior_NPCType DefaultNPCType = EPS_AI_Behavior_NPCType::Civilian;
// Behavior Tree à utiliser (peut être overridé par l'AIController)
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Behavior")
TSoftObjectPtr<UBehaviorTree> DefaultBehaviorTree;
// Helper
float GetTrait(EPS_AI_Behavior_TraitAxis Axis) const;
};
```
### 2.3 PersonalityComponent (`PS_AI_Behavior_PersonalityComponent.h`)
Attaché au Pawn. Fournit l'accès runtime aux traits, modifie les seuils dynamiquement.
```cpp
UCLASS(ClassGroup="PS AI Behavior", meta=(BlueprintSpawnableComponent))
class PS_AI_BEHAVIOR_API UPS_AI_Behavior_PersonalityComponent : public UActorComponent
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Personality")
UPS_AI_Behavior_PersonalityProfile* Profile;
// Runtime overrides (initialisés depuis Profile au BeginPlay)
UPROPERTY(BlueprintReadWrite, Category="Personality|Runtime")
TMap<EPS_AI_Behavior_TraitAxis, float> RuntimeTraits;
// Threat level perçu (mis à jour par BTService_UpdateThreat)
UPROPERTY(BlueprintReadWrite, Category="Personality|Runtime")
float PerceivedThreatLevel = 0.0f;
// Décision finale basée sur traits + threat
UFUNCTION(BlueprintCallable, Category="PS AI Behavior|Personality")
EPS_AI_Behavior_State EvaluateReaction() const;
UFUNCTION(BlueprintCallable, Category="PS AI Behavior|Personality")
float GetTrait(EPS_AI_Behavior_TraitAxis Axis) const;
UFUNCTION(BlueprintCallable, Category="PS AI Behavior|Personality")
void ModifyTrait(EPS_AI_Behavior_TraitAxis Axis, float Delta);
};
```
**Logique `EvaluateReaction()`** :
```
EffectiveCourage = RuntimeTraits[Courage] * (1 - PerceivedThreatLevel * 0.5)
if PerceivedThreatLevel > FleeThreshold * (1 + EffectiveCourage) → Fleeing
if PerceivedThreatLevel > AttackThreshold * (1 - Aggressivity) → Combat
if PerceivedThreatLevel > 0.1 → Alerted
else → Idle/Patrol
```
### 2.4 AIController (`PS_AI_Behavior_AIController.h`)
```cpp
UCLASS()
class PS_AI_BEHAVIOR_API APS_AI_Behavior_AIController : public AAIController
{
GENERATED_BODY()
public:
APS_AI_Behavior_AIController();
// Blackboard keys (nom constants)
static const FName BB_State; // EPS_AI_Behavior_State
static const FName BB_ThreatActor; // UObject*
static const FName BB_ThreatLocation; // FVector
static const FName BB_ThreatLevel; // float
static const FName BB_CoverLocation; // FVector
static const FName BB_PatrolIndex; // int32
static const FName BB_HomeLocation; // FVector
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Behavior")
UBehaviorTree* BehaviorTreeAsset;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Behavior")
UBlackboardData* BlackboardAsset;
// Patrol waypoints (set par level designer ou spawner)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Patrol")
TArray<FVector> PatrolPoints;
protected:
virtual void OnPossess(APawn* InPawn) override;
virtual void OnUnPossess() override;
// Auto-détection optionnelle de PS_AI_ConvAgent
void TryBindConversationAgent();
};
```
**`OnPossess`** :
1. Trouve `PersonalityComponent` sur le Pawn
2. Crée/initialise le Blackboard
3. Lit `DefaultBehaviorTree` du ProfileData (ou utilise `BehaviorTreeAsset`)
4. Lance `RunBehaviorTree()`
5. Appelle `TryBindConversationAgent()`
**`TryBindConversationAgent()`** :
- Via `FindComponentByClass` (pas de include direct, utilise `FindObject` ou interface)
- Si trouvé : bind OnAgentActionRequested pour injecter des actions dans le BT
### 2.5 PerceptionComponent (`PS_AI_Behavior_PerceptionComponent.h`)
Wrapper configuré autour de `UAIPerceptionComponent` :
```cpp
UCLASS(ClassGroup="PS AI Behavior", meta=(BlueprintSpawnableComponent))
class PS_AI_BEHAVIOR_API UPS_AI_Behavior_PerceptionComponent : public UAIPerceptionComponent
{
GENERATED_BODY()
public:
UPS_AI_Behavior_PerceptionComponent();
// Pré-configure : Sight (60m, 90° FOV) + Hearing (30m) + Damage
virtual void BeginPlay() override;
UFUNCTION(BlueprintCallable, Category="PS AI Behavior|Perception")
AActor* GetHighestThreat() const;
UFUNCTION(BlueprintCallable, Category="PS AI Behavior|Perception")
float CalculateThreatLevel() const;
protected:
UFUNCTION()
void OnPerceptionUpdated(const TArray<AActor*>& UpdatedActors);
};
```
### 2.6 CombatComponent (`PS_AI_Behavior_CombatComponent.h`)
Gère l'état combat, les distances, le cooldown d'attaque :
```cpp
UCLASS(ClassGroup="PS AI Behavior", meta=(BlueprintSpawnableComponent))
class PS_AI_BEHAVIOR_API UPS_AI_Behavior_CombatComponent : public UActorComponent
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Combat")
float AttackRange = 200.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Combat")
float AttackCooldown = 1.5f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Combat")
float AttackDamage = 20.0f;
UFUNCTION(BlueprintCallable, Category="PS AI Behavior|Combat")
bool CanAttack() const;
UFUNCTION(BlueprintCallable, Category="PS AI Behavior|Combat")
void ExecuteAttack(AActor* Target);
UFUNCTION(BlueprintCallable, Category="PS AI Behavior|Combat")
bool IsInAttackRange(AActor* Target) const;
};
```
---
## 3. Behavior Tree — Nodes
### 3.1 Services (tournent en continu)
**BTService_UpdateThreat** :
- Lit `PerceptionComponent::CalculateThreatLevel()`
- Écrit `BB_ThreatLevel`, `BB_ThreatActor`, `BB_ThreatLocation`
- Met à jour `PersonalityComponent::PerceivedThreatLevel`
**BTService_EvaluateReaction** :
- Appelle `PersonalityComponent::EvaluateReaction()`
- Écrit `BB_State` dans le Blackboard
- Le BT utilise des decorators pour brancher sur cet état
### 3.2 Tasks
**BTTask_Patrol** :
- Lit `BB_PatrolIndex`, navigue vers `PatrolPoints[idx]`
- Au succès, incrémente l'index (cyclique)
- Supporte pause aléatoire aux waypoints
**BTTask_FleeFrom** :
- Lit `BB_ThreatLocation`
- Utilise EQS `EQS_FindFleePoint` (direction opposée à la menace)
- `MoveTo()` vers le point trouvé
**BTTask_FindCover** :
- Lance EQS `EQS_FindCover` (scoring : distance menace, line-of-sight block, distance au NPC)
- Navigue vers le meilleur point
- Écrit `BB_CoverLocation`
**BTTask_Attack** :
- Vérifie `CombatComponent::CanAttack()`
- Si hors range : `MoveTo(Target)`
- Si in range : `CombatComponent::ExecuteAttack(Target)`
### 3.3 Decorators
**BTDecorator_CheckTrait** :
- Paramètres : `TraitAxis`, `ComparisonOp` (>, <, ==), `Threshold`
- Lit le trait depuis `PersonalityComponent`
- Exemple : "Exécuter seulement si Courage > 0.5"
---
## 4. EQS
### EQS_FindCover
- **Generator** : Points sur grille autour du NPC (rayon 15m)
- **Tests** :
- Distance à la menace (préfère mi-distance, pas trop loin)
- Trace visibility (préfère les points non-visibles depuis la menace)
- Distance au NPC (préfère les points proches)
- `EQSTest_CoverQuality` (custom) : raycasts multiples pour évaluer la qualité de couverture
### EQS_FindFleePoint
- **Generator** : Points sur donut (rayon 10-25m)
- **Tests** :
- Dot product direction (opposé à la menace : score max)
- PathExistence (doit être atteignable sur NavMesh)
- Distance à la menace (préfère loin)
### EQS_FindPatrolPoint
- **Generator** : Points depuis la liste PatrolPoints de l'AIController
- **Tests** :
- Distance au NPC (préfère le plus proche non-visité)
### EQSContext_Threat
- Renvoie l'acteur/location de `BB_ThreatActor` / `BB_ThreatLocation`
---
## 5. Intégration optionnelle PS_AI_ConvAgent
**Mécanisme** : Pas de `#include` direct. L'AIController utilise `FindComponentByClass` avec le nom de classe via UObject reflection :
```cpp
void APS_AI_Behavior_AIController::TryBindConversationAgent()
{
// Soft reference — no link-time dependency
UActorComponent* ConvComp = GetPawn()->FindComponentByClass(
LoadClass<UActorComponent>(nullptr,
TEXT("/Script/PS_AI_ConvAgent.PS_AI_ConvAgent_ElevenLabsComponent")));
if (ConvComp)
{
// Bind to OnAgentActionRequested via dynamic delegate
// Actions from conversation can inject BT state changes
}
}
```
Cela permet :
- Un NPC conversationnel qui reçoit "Fuis !" via ElevenLabs → injecte State=Fleeing dans le BT
- Aucune dépendance de compilation
---
## 6. Build.cs — Dépendances
```csharp
// PS_AI_Behavior.Build.cs
PublicDependencyModuleNames.AddRange(new string[] {
"Core", "CoreUObject", "Engine",
"AIModule", // AAIController, BehaviorTree, Blackboard
"GameplayTasks", // UGameplayTask (requis par BT tasks)
"NavigationSystem", // NavMesh queries
});
PrivateDependencyModuleNames.AddRange(new string[] {
"Settings", // ISettingsModule
});
// PAS de dépendance vers PS_AI_ConvAgent
```
---
## 7. Ordre d'implémentation (étapes)
### Étape 1 — Squelette plugin
- [ ] Créer la structure de fichiers du plugin
- [ ] `.uplugin`, `Build.cs`, module class
- [ ] `Definitions.h` (enums, structs, log category)
- [ ] `Settings.h/cpp` (settings vides pour l'instant)
- [ ] Ajouter au `.uproject`
- [ ] **Vérification** : compile sans erreur
### Étape 2 — Personality System
- [ ] `PersonalityProfile` (Data Asset)
- [ ] `PersonalityComponent` avec `EvaluateReaction()`
- [ ] **Vérification** : peut créer un Data Asset dans l'éditeur, lire les traits en BP
### Étape 3 — AIController + Perception
- [ ] `PS_AI_Behavior_AIController` avec Blackboard setup
- [ ] `PS_AI_Behavior_PerceptionComponent` (sight + hearing)
- [ ] `BlackboardData` asset par défaut
- [ ] **Vérification** : un NPC spawné détecte les acteurs proches
### Étape 4 — BT Services + Decorators
- [ ] `BTService_UpdateThreat`
- [ ] `BTService_EvaluateReaction`
- [ ] `BTDecorator_CheckTrait`
- [ ] **Vérification** : le Blackboard se met à jour en jeu
### Étape 5 — BT Tasks (Navigation)
- [ ] `BTTask_Patrol`
- [ ] `BTTask_FleeFrom`
- [ ] `BTTask_FindCover`
- [ ] **Vérification** : NPC patrouille et fuit
### Étape 6 — Combat
- [ ] `CombatComponent`
- [ ] `BTTask_Attack`
- [ ] **Vérification** : NPC ennemi attaque le joueur
### Étape 7 — EQS
- [ ] `EQSContext_Threat`
- [ ] `EQSTest_CoverQuality`
- [ ] Assets EQS dans Content/
- [ ] **Vérification** : NPC trouve des couvertures intelligemment
### Étape 8 — Intégration ConvAgent (optionnelle)
- [ ] `TryBindConversationAgent()` soft binding
- [ ] Test avec un NPC qui a les deux plugins
---
## 8. Résumé des fichiers à créer
| # | Fichier | Rôle |
|---|---------|------|
| 1 | `PS_AI_Behavior.uplugin` | Plugin descriptor |
| 2 | `PS_AI_Behavior.Build.cs` | Module dependencies |
| 3 | `PS_AI_Behavior.h / .cpp` | Module class (register settings) |
| 4 | `PS_AI_Behavior_Definitions.h` | Enums, structs, log |
| 5 | `PS_AI_Behavior_Settings.h / .cpp` | Project settings |
| 6 | `PS_AI_Behavior_PersonalityProfile.h / .cpp` | Data Asset |
| 7 | `PS_AI_Behavior_PersonalityComponent.h / .cpp` | Personality runtime |
| 8 | `PS_AI_Behavior_AIController.h / .cpp` | AIController |
| 9 | `PS_AI_Behavior_PerceptionComponent.h / .cpp` | AI Perception |
| 10 | `PS_AI_Behavior_CombatComponent.h / .cpp` | Combat state |
| 11 | `BT/PS_AI_Behavior_BTTask_Patrol.h / .cpp` | Patrol task |
| 12 | `BT/PS_AI_Behavior_BTTask_FleeFrom.h / .cpp` | Flee task |
| 13 | `BT/PS_AI_Behavior_BTTask_FindCover.h / .cpp` | Cover task |
| 14 | `BT/PS_AI_Behavior_BTTask_Attack.h / .cpp` | Attack task |
| 15 | `BT/PS_AI_Behavior_BTService_UpdateThreat.h / .cpp` | Threat service |
| 16 | `BT/PS_AI_Behavior_BTService_EvaluateReaction.h / .cpp` | Reaction service |
| 17 | `BT/PS_AI_Behavior_BTDecorator_CheckTrait.h / .cpp` | Trait decorator |
| 18 | `EQS/PS_AI_Behavior_EQSContext_Threat.h / .cpp` | EQS context |
| 19 | `EQS/PS_AI_Behavior_EQSTest_CoverQuality.h / .cpp` | EQS test |
**Total : ~38 fichiers C++ (19 paires h/cpp) + 1 .uplugin + 1 .Build.cs**

View File

@ -30,10 +30,5 @@
] ]
} }
], ],
"Plugins": [ "Plugins": []
{
"Name": "NavigationSystem",
"Enabled": true
}
]
} }

View File

@ -28,7 +28,7 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_FindCover::ExecuteTask(
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent(); UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
if (!BB) return EBTNodeResult::Failed; if (!BB) return EBTNodeResult::Failed;
const UWorld* World = GetWorld(); UWorld* World = GetWorld();
if (!World) return EBTNodeResult::Failed; if (!World) return EBTNodeResult::Failed;
const FVector NpcLoc = AIC->GetPawn()->GetActorLocation(); const FVector NpcLoc = AIC->GetPawn()->GetActorLocation();

View File

@ -42,18 +42,11 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_FollowSpline::ExecuteTask(
Follower->ResumeFollowing(); Follower->ResumeFollowing();
} }
// Listen for end-of-spline // Initialize memory — TickTask will poll bIsFollowing to detect end-of-spline
FFollowMemory* Memory = reinterpret_cast<FFollowMemory*>(NodeMemory); FFollowMemory* Memory = reinterpret_cast<FFollowMemory*>(NodeMemory);
Memory->Elapsed = 0.0f; Memory->Elapsed = 0.0f;
Memory->bEndReached = false; Memory->bEndReached = false;
// Bind to end delegate
Follower->OnSplineEndReached.AddWeakLambda(this,
[Memory](APS_AI_Behavior_SplinePath* /*Spline*/)
{
Memory->bEndReached = true;
});
return EBTNodeResult::InProgress; return EBTNodeResult::InProgress;
} }
@ -62,11 +55,17 @@ void UPS_AI_Behavior_BTTask_FollowSpline::TickTask(
{ {
FFollowMemory* Memory = reinterpret_cast<FFollowMemory*>(NodeMemory); FFollowMemory* Memory = reinterpret_cast<FFollowMemory*>(NodeMemory);
// Check if spline end was reached // Check if spline end was reached (poll bIsFollowing — set to false by SplineFollowerComponent)
if (Memory->bEndReached) AAIController* AICCheck = OwnerComp.GetAIOwner();
if (AICCheck && AICCheck->GetPawn())
{ {
FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded); UPS_AI_Behavior_SplineFollowerComponent* FollowerCheck =
return; AICCheck->GetPawn()->FindComponentByClass<UPS_AI_Behavior_SplineFollowerComponent>();
if (FollowerCheck && !FollowerCheck->bIsFollowing)
{
FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
return;
}
} }
// Time limit check // Time limit check
@ -91,20 +90,13 @@ void UPS_AI_Behavior_BTTask_FollowSpline::TickTask(
} }
} }
// Verify follower is still active // Verify pawn is still valid
AAIController* AIC = OwnerComp.GetAIOwner(); AAIController* AIC = OwnerComp.GetAIOwner();
if (!AIC || !AIC->GetPawn()) if (!AIC || !AIC->GetPawn())
{ {
FinishLatentTask(OwnerComp, EBTNodeResult::Failed); FinishLatentTask(OwnerComp, EBTNodeResult::Failed);
return; return;
} }
UPS_AI_Behavior_SplineFollowerComponent* Follower =
AIC->GetPawn()->FindComponentByClass<UPS_AI_Behavior_SplineFollowerComponent>();
if (!Follower || !Follower->bIsFollowing)
{
FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
}
} }
EBTNodeResult::Type UPS_AI_Behavior_BTTask_FollowSpline::AbortTask( EBTNodeResult::Type UPS_AI_Behavior_BTTask_FollowSpline::AbortTask(

View File

@ -34,7 +34,7 @@ void UPS_AI_Behavior_EQSGenerator_CoverPoints::GenerateItems(FEnvQueryInstance&
{ {
if (QuerierPawn->Implements<UPS_AI_Behavior_Interface>()) if (QuerierPawn->Implements<UPS_AI_Behavior_Interface>())
{ {
NPCType = IPS_AI_Behavior::Execute_GetBehaviorNPCType(const_cast<APawn*>(QuerierPawn)); NPCType = IPS_AI_Behavior_Interface::Execute_GetBehaviorNPCType(const_cast<APawn*>(QuerierPawn));
} }
else if (const auto* PC = QuerierPawn->FindComponentByClass<UPS_AI_Behavior_PersonalityComponent>()) else if (const auto* PC = QuerierPawn->FindComponentByClass<UPS_AI_Behavior_PersonalityComponent>())
{ {

View File

@ -71,8 +71,8 @@ void UPS_AI_Behavior_EQSTest_CoverQuality::RunTest(FEnvQueryInstance& QueryInsta
} }
// Score: ratio of blocked traces (0.0 = fully exposed, 1.0 = fully covered) // Score: ratio of blocked traces (0.0 = fully exposed, 1.0 = fully covered)
const float Score = BlockedCount / TraceHeights.Num(); const float Score = BlockedCount / static_cast<float>(TraceHeights.Num());
It.SetScore(TestPurpose, FilterType, Score); It.SetScore(TestPurpose, FilterType, Score, 0.0f, 1.0f);
} }
} }

View File

@ -47,10 +47,10 @@ void APS_AI_Behavior_AIController::OnPossess(APawn* InPawn)
if (InPawn->Implements<UPS_AI_Behavior_Interface>()) if (InPawn->Implements<UPS_AI_Behavior_Interface>())
{ {
// Use the interface — the host project controls the storage // Use the interface — the host project controls the storage
NPCType = IPS_AI_Behavior::Execute_GetBehaviorNPCType(InPawn); NPCType = IPS_AI_Behavior_Interface::Execute_GetBehaviorNPCType(InPawn);
// Also check if the interface provides a specific TeamId // Also check if the interface provides a specific TeamId
const uint8 InterfaceTeamId = IPS_AI_Behavior::Execute_GetBehaviorTeamId(InPawn); const uint8 InterfaceTeamId = IPS_AI_Behavior_Interface::Execute_GetBehaviorTeamId(InPawn);
if (InterfaceTeamId != FGenericTeamId::NoTeam) if (InterfaceTeamId != FGenericTeamId::NoTeam)
{ {
TeamId = InterfaceTeamId; TeamId = InterfaceTeamId;
@ -73,7 +73,7 @@ void APS_AI_Behavior_AIController::OnPossess(APawn* InPawn)
case EPS_AI_Behavior_NPCType::Enemy: case EPS_AI_Behavior_NPCType::Enemy:
// Check if infiltrated (hostile=false → disguised as civilian) // Check if infiltrated (hostile=false → disguised as civilian)
if (InPawn->Implements<UPS_AI_Behavior_Interface>() && if (InPawn->Implements<UPS_AI_Behavior_Interface>() &&
!IPS_AI_Behavior::Execute_IsBehaviorHostile(InPawn)) !IPS_AI_Behavior_Interface::Execute_IsBehaviorHostile(InPawn))
{ {
TeamId = 1; // Disguised as Civilian TeamId = 1; // Disguised as Civilian
} }
@ -187,7 +187,9 @@ void APS_AI_Behavior_AIController::SetupBlackboard()
BlackboardAsset->Keys.Add(SplineProgressEntry); BlackboardAsset->Keys.Add(SplineProgressEntry);
} }
UseBlackboard(BlackboardAsset, Blackboard); UBlackboardComponent* RawBBComp = nullptr;
UseBlackboard(BlackboardAsset, RawBBComp);
Blackboard = RawBBComp;
// Initialize home location to pawn's spawn position // Initialize home location to pawn's spawn position
if (Blackboard && GetPawn()) if (Blackboard && GetPawn())
@ -280,7 +282,7 @@ ETeamAttitude::Type APS_AI_Behavior_AIController::GetTeamAttitudeTowards(const A
// Check via IPS_AI_Behavior interface // Check via IPS_AI_Behavior interface
else if (OtherPawn->Implements<UPS_AI_Behavior_Interface>()) else if (OtherPawn->Implements<UPS_AI_Behavior_Interface>())
{ {
OtherTeam = IPS_AI_Behavior::Execute_GetBehaviorTeamId(const_cast<APawn*>(OtherPawn)); OtherTeam = IPS_AI_Behavior_Interface::Execute_GetBehaviorTeamId(const_cast<APawn*>(OtherPawn));
} }
} }

View File

@ -163,7 +163,7 @@ void APS_AI_Behavior_CoverPoint::UpdateVisualization()
#if WITH_EDITORONLY_DATA #if WITH_EDITORONLY_DATA
if (!ArrowComp) return; if (!ArrowComp) return;
FLinearColor Color; FLinearColor Color = FLinearColor::White;
switch (PointType) switch (PointType)
{ {
case EPS_AI_Behavior_CoverPointType::Cover: case EPS_AI_Behavior_CoverPointType::Cover:
@ -172,6 +172,8 @@ void APS_AI_Behavior_CoverPoint::UpdateVisualization()
case EPS_AI_Behavior_CoverPointType::HidingSpot: case EPS_AI_Behavior_CoverPointType::HidingSpot:
Color = FLinearColor(1.0f, 0.85f, 0.0f); // Yellow Color = FLinearColor(1.0f, 0.85f, 0.0f); // Yellow
break; break;
default:
break;
} }
if (!bEnabled) if (!bEnabled)

View File

@ -87,7 +87,7 @@ EPS_AI_Behavior_TargetType UPS_AI_Behavior_PerceptionComponent::ClassifyActor(co
if (Actor->Implements<UPS_AI_Behavior_Interface>()) if (Actor->Implements<UPS_AI_Behavior_Interface>())
{ {
const EPS_AI_Behavior_NPCType NPCType = const EPS_AI_Behavior_NPCType NPCType =
IPS_AI_Behavior::Execute_GetBehaviorNPCType(const_cast<AActor*>(Actor)); IPS_AI_Behavior_Interface::Execute_GetBehaviorNPCType(const_cast<AActor*>(Actor));
switch (NPCType) switch (NPCType)
{ {
@ -118,7 +118,7 @@ EPS_AI_Behavior_TargetType UPS_AI_Behavior_PerceptionComponent::ClassifyActor(co
// ─── Target Selection ─────────────────────────────────────────────────────── // ─── Target Selection ───────────────────────────────────────────────────────
AActor* UPS_AI_Behavior_PerceptionComponent::GetHighestThreatActor() const AActor* UPS_AI_Behavior_PerceptionComponent::GetHighestThreatActor()
{ {
// Get priority from PersonalityProfile if available // Get priority from PersonalityProfile if available
TArray<EPS_AI_Behavior_TargetType> Priority; TArray<EPS_AI_Behavior_TargetType> Priority;
@ -146,7 +146,7 @@ AActor* UPS_AI_Behavior_PerceptionComponent::GetHighestThreatActor() const
} }
AActor* UPS_AI_Behavior_PerceptionComponent::GetHighestThreatActor( AActor* UPS_AI_Behavior_PerceptionComponent::GetHighestThreatActor(
const TArray<EPS_AI_Behavior_TargetType>& TargetPriority) const const TArray<EPS_AI_Behavior_TargetType>& TargetPriority)
{ {
// Gather all perceived actors from all senses // Gather all perceived actors from all senses
TArray<AActor*> PerceivedActors; TArray<AActor*> PerceivedActors;
@ -241,7 +241,7 @@ AActor* UPS_AI_Behavior_PerceptionComponent::GetHighestThreatActor(
return BestThreat; return BestThreat;
} }
float UPS_AI_Behavior_PerceptionComponent::CalculateThreatLevel() const float UPS_AI_Behavior_PerceptionComponent::CalculateThreatLevel()
{ {
const AActor* Owner = GetOwner(); const AActor* Owner = GetOwner();
if (!Owner) return 0.0f; if (!Owner) return 0.0f;
@ -252,7 +252,7 @@ float UPS_AI_Behavior_PerceptionComponent::CalculateThreatLevel() const
TArray<AActor*> PerceivedActors; TArray<AActor*> PerceivedActors;
GetCurrentlyPerceivedActors(nullptr, PerceivedActors); // All senses GetCurrentlyPerceivedActors(nullptr, PerceivedActors); // All senses
for (const AActor* Actor : PerceivedActors) for (AActor* Actor : PerceivedActors)
{ {
if (!Actor) continue; if (!Actor) continue;
@ -293,7 +293,7 @@ float UPS_AI_Behavior_PerceptionComponent::CalculateThreatLevel() const
return FMath::Min(TotalThreat, 2.0f); return FMath::Min(TotalThreat, 2.0f);
} }
bool UPS_AI_Behavior_PerceptionComponent::GetThreatLocation(FVector& OutLocation) const bool UPS_AI_Behavior_PerceptionComponent::GetThreatLocation(FVector& OutLocation)
{ {
AActor* Threat = GetHighestThreatActor(); AActor* Threat = GetHighestThreatActor();
if (Threat) if (Threat)

View File

@ -151,7 +151,7 @@ void UPS_AI_Behavior_PersonalityComponent::OnRep_CurrentState(EPS_AI_Behavior_St
AActor* Owner = GetOwner(); AActor* Owner = GetOwner();
if (Owner && Owner->Implements<UPS_AI_Behavior_Interface>()) if (Owner && Owner->Implements<UPS_AI_Behavior_Interface>())
{ {
IPS_AI_Behavior::Execute_OnBehaviorStateChanged(Owner, CurrentState, OldState); IPS_AI_Behavior_Interface::Execute_OnBehaviorStateChanged(Owner, CurrentState, OldState);
} }
} }
@ -168,10 +168,10 @@ void UPS_AI_Behavior_PersonalityComponent::HandleStateChanged(
if (Owner->Implements<UPS_AI_Behavior_Interface>()) if (Owner->Implements<UPS_AI_Behavior_Interface>())
{ {
float NewSpeed = Profile ? Profile->GetSpeedForState(NewState) : 150.0f; float NewSpeed = Profile ? Profile->GetSpeedForState(NewState) : 150.0f;
IPS_AI_Behavior::Execute_SetBehaviorMovementSpeed(Owner, NewSpeed); IPS_AI_Behavior_Interface::Execute_SetBehaviorMovementSpeed(Owner, NewSpeed);
// 3. Notify the Pawn of the state change // 3. Notify the Pawn of the state change
IPS_AI_Behavior::Execute_OnBehaviorStateChanged(Owner, NewState, OldState); IPS_AI_Behavior_Interface::Execute_OnBehaviorStateChanged(Owner, NewState, OldState);
} }
} }
@ -181,7 +181,7 @@ EPS_AI_Behavior_NPCType UPS_AI_Behavior_PersonalityComponent::GetNPCType() const
AActor* Owner = GetOwner(); AActor* Owner = GetOwner();
if (Owner && Owner->Implements<UPS_AI_Behavior_Interface>()) if (Owner && Owner->Implements<UPS_AI_Behavior_Interface>())
{ {
return IPS_AI_Behavior::Execute_GetBehaviorNPCType(Owner); return IPS_AI_Behavior_Interface::Execute_GetBehaviorNPCType(Owner);
} }
// Fallback: read from PersonalityProfile // Fallback: read from PersonalityProfile

View File

@ -4,6 +4,7 @@
#include "PS_AI_Behavior_SplinePath.h" #include "PS_AI_Behavior_SplinePath.h"
#include "PS_AI_Behavior_SplineNetwork.h" #include "PS_AI_Behavior_SplineNetwork.h"
#include "PS_AI_Behavior_PersonalityComponent.h" #include "PS_AI_Behavior_PersonalityComponent.h"
#include "Components/SplineComponent.h"
#include "GameFramework/Character.h" #include "GameFramework/Character.h"
#include "GameFramework/CharacterMovementComponent.h" #include "GameFramework/CharacterMovementComponent.h"
#include "Net/UnrealNetwork.h" #include "Net/UnrealNetwork.h"

View File

@ -11,22 +11,10 @@
void UPS_AI_Behavior_SplineNetwork::Initialize(FSubsystemCollectionBase& Collection) void UPS_AI_Behavior_SplineNetwork::Initialize(FSubsystemCollectionBase& Collection)
{ {
Super::Initialize(Collection); Super::Initialize(Collection);
UWorld* World = GetWorld();
if (World)
{
BeginPlayHandle = World->OnWorldBeginPlay.AddUObject(this, &UPS_AI_Behavior_SplineNetwork::OnWorldBeginPlay);
}
} }
void UPS_AI_Behavior_SplineNetwork::Deinitialize() void UPS_AI_Behavior_SplineNetwork::Deinitialize()
{ {
UWorld* World = GetWorld();
if (World && BeginPlayHandle.IsValid())
{
World->OnWorldBeginPlay.Remove(BeginPlayHandle);
}
AllSplines.Empty(); AllSplines.Empty();
TotalJunctions = 0; TotalJunctions = 0;
Super::Deinitialize(); Super::Deinitialize();
@ -34,6 +22,7 @@ void UPS_AI_Behavior_SplineNetwork::Deinitialize()
void UPS_AI_Behavior_SplineNetwork::OnWorldBeginPlay(UWorld& InWorld) void UPS_AI_Behavior_SplineNetwork::OnWorldBeginPlay(UWorld& InWorld)
{ {
Super::OnWorldBeginPlay(InWorld);
RebuildNetwork(); RebuildNetwork();
} }

View File

@ -4,6 +4,7 @@
#include "CoreMinimal.h" #include "CoreMinimal.h"
#include "BehaviorTree/BTTaskNode.h" #include "BehaviorTree/BTTaskNode.h"
#include "PS_AI_Behavior_Definitions.h"
#include "PS_AI_Behavior_BTTask_FindCover.generated.h" #include "PS_AI_Behavior_BTTask_FindCover.generated.h"
class APS_AI_Behavior_CoverPoint; class APS_AI_Behavior_CoverPoint;

View File

@ -81,7 +81,7 @@ public:
// ─── Runtime (server-only) ────────────────────────────────────────── // ─── Runtime (server-only) ──────────────────────────────────────────
/** Current occupants. Managed by the BT / EQS. */ /** Current occupants. Managed by the BT / EQS. */
UPROPERTY(Transient, BlueprintReadOnly, Category = "Cover Point|Runtime") UPROPERTY(Transient)
TArray<TWeakObjectPtr<AActor>> CurrentOccupants; TArray<TWeakObjectPtr<AActor>> CurrentOccupants;
// ─── API ──────────────────────────────────────────────────────────── // ─── API ────────────────────────────────────────────────────────────

View File

@ -7,7 +7,7 @@
// ─── Log Category ─────────────────────────────────────────────────────────── // ─── Log Category ───────────────────────────────────────────────────────────
DECLARE_LOG_CATEGORY_EXTERN(LogPS_AI_Behavior, Log, All); PS_AI_BEHAVIOR_API DECLARE_LOG_CATEGORY_EXTERN(LogPS_AI_Behavior, Log, All);
// ─── API Macro ────────────────────────────────────────────────────────────── // ─── API Macro ──────────────────────────────────────────────────────────────

View File

@ -36,7 +36,7 @@ class PS_AI_BEHAVIOR_API UPS_AI_Behavior_Interface : public UInterface
GENERATED_BODY() GENERATED_BODY()
}; };
class PS_AI_BEHAVIOR_API IPS_AI_Behavior class PS_AI_BEHAVIOR_API IPS_AI_Behavior_Interface
{ {
GENERATED_BODY() GENERATED_BODY()

View File

@ -33,17 +33,17 @@ public:
* @return The most threatening actor, or nullptr if none perceived. * @return The most threatening actor, or nullptr if none perceived.
*/ */
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Perception") UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Perception")
AActor* GetHighestThreatActor(const TArray<EPS_AI_Behavior_TargetType>& TargetPriority) const; AActor* GetHighestThreatActor(const TArray<EPS_AI_Behavior_TargetType>& TargetPriority);
/** Convenience overload — reads priority from the Pawn's PersonalityProfile. */ /** Convenience overload — reads priority from the Pawn's PersonalityProfile. */
AActor* GetHighestThreatActor() const; AActor* GetHighestThreatActor();
/** /**
* Compute an aggregate threat level from all currently perceived hostile stimuli. * Compute an aggregate threat level from all currently perceived hostile stimuli.
* Returns 0.0 (no threat) to 1.0+ (extreme danger). * Returns 0.0 (no threat) to 1.0+ (extreme danger).
*/ */
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Perception") UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Perception")
float CalculateThreatLevel() const; float CalculateThreatLevel();
/** /**
* Get the location of the last known threat stimulus. * Get the location of the last known threat stimulus.
@ -51,7 +51,7 @@ public:
* @return True if a threat was found. * @return True if a threat was found.
*/ */
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Perception") UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Perception")
bool GetThreatLocation(FVector& OutLocation) const; bool GetThreatLocation(FVector& OutLocation);
protected: protected:
virtual void BeginPlay() override; virtual void BeginPlay() override;

View File

@ -103,8 +103,6 @@ private:
void DetectJunctions(APS_AI_Behavior_SplinePath* SplineA, void DetectJunctions(APS_AI_Behavior_SplinePath* SplineA,
APS_AI_Behavior_SplinePath* SplineB, float Tolerance); APS_AI_Behavior_SplinePath* SplineB, float Tolerance);
/** World init callback. */ /** UWorldSubsystem override — called when world begins play. */
void OnWorldBeginPlay(UWorld& InWorld); virtual void OnWorldBeginPlay(UWorld& InWorld) override;
FDelegateHandle BeginPlayHandle;
}; };

View File

@ -54,9 +54,9 @@ void FPS_AI_Behavior_SplineEdMode::Exit()
} }
bool FPS_AI_Behavior_SplineEdMode::HandleClick( bool FPS_AI_Behavior_SplineEdMode::HandleClick(
FEditorViewportClient* InViewportClient, HHitProxy* HitProxy, const FInputClick& Click) FEditorViewportClient* InViewportClient, HHitProxy* HitProxy, const FViewportClick& Click)
{ {
if (Click.Key != EKeys::LeftMouseButton) if (Click.GetKey() != EKeys::LeftMouseButton)
{ {
return false; return false;
} }
@ -76,9 +76,6 @@ bool FPS_AI_Behavior_SplineEdMode::HandleClick(
InViewportClient->EngineShowFlags)); InViewportClient->EngineShowFlags));
FSceneView* View = InViewportClient->CalcSceneView(&ViewFamily); FSceneView* View = InViewportClient->CalcSceneView(&ViewFamily);
const FVector WorldOrigin = View->ViewMatrices.GetViewOrigin();
FVector WorldDirection;
// Deproject mouse to world // Deproject mouse to world
FVector2D MousePos(HitX, HitY); FVector2D MousePos(HitX, HitY);
FVector RayOrigin, RayDirection; FVector RayOrigin, RayDirection;
@ -100,7 +97,7 @@ bool FPS_AI_Behavior_SplineEdMode::HandleClick(
FVector ClickLocation = Hit.ImpactPoint; FVector ClickLocation = Hit.ImpactPoint;
// Ctrl+Click on existing spline → select for extension // Ctrl+Click on existing spline → select for extension
if (Click.bControlDown) if (Click.IsControlDown())
{ {
// Check if we hit a SplinePath // Check if we hit a SplinePath
AActor* HitActor = Hit.GetActor(); AActor* HitActor = Hit.GetActor();

View File

@ -37,7 +37,7 @@ public:
virtual void Exit() override; virtual void Exit() override;
virtual bool HandleClick(FEditorViewportClient* InViewportClient, virtual bool HandleClick(FEditorViewportClient* InViewportClient,
HHitProxy* HitProxy, const FInputClick& Click) override; HHitProxy* HitProxy, const FViewportClick& Click) override;
virtual bool InputKey(FEditorViewportClient* ViewportClient, virtual bool InputKey(FEditorViewportClient* ViewportClient,
FViewport* Viewport, FKey Key, EInputEvent Event) override; FViewport* Viewport, FKey Key, EInputEvent Event) override;