Compare commits
2 Commits
a07ac36686
...
909583a1bb
| Author | SHA1 | Date | |
|---|---|---|---|
| 909583a1bb | |||
| 5d5b85380a |
@ -40,6 +40,10 @@
|
||||
{
|
||||
"Name": "AudioCapture",
|
||||
"Enabled": true
|
||||
},
|
||||
{
|
||||
"Name": "PS_AI_Behavior",
|
||||
"Enabled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
511
Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/PLAN.md
Normal file
511
Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/PLAN.md
Normal 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**
|
||||
@ -0,0 +1,34 @@
|
||||
{
|
||||
"FileVersion": 3,
|
||||
"Version": 1,
|
||||
"VersionName": "1.0.0",
|
||||
"FriendlyName": "PS AI Behavior",
|
||||
"Description": "NPC behavior system using Behavior Trees, EQS, and personality-driven reactions for civilians and enemies.",
|
||||
"Category": "AI",
|
||||
"CreatedBy": "Asterion",
|
||||
"CanContainContent": true,
|
||||
"IsBetaVersion": true,
|
||||
"Modules": [
|
||||
{
|
||||
"Name": "PS_AI_Behavior",
|
||||
"Type": "Runtime",
|
||||
"LoadingPhase": "PreDefault",
|
||||
"PlatformAllowList": [
|
||||
"Win64",
|
||||
"Mac",
|
||||
"Linux"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "PS_AI_BehaviorEditor",
|
||||
"Type": "UncookedOnly",
|
||||
"LoadingPhase": "Default",
|
||||
"PlatformAllowList": [
|
||||
"Win64",
|
||||
"Mac",
|
||||
"Linux"
|
||||
]
|
||||
}
|
||||
],
|
||||
"Plugins": []
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
using UnrealBuildTool;
|
||||
|
||||
public class PS_AI_Behavior : ModuleRules
|
||||
{
|
||||
public PS_AI_Behavior(ReadOnlyTargetRules Target) : base(Target)
|
||||
{
|
||||
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||
|
||||
PublicDependencyModuleNames.AddRange(new string[]
|
||||
{
|
||||
"Core",
|
||||
"CoreUObject",
|
||||
"Engine",
|
||||
"InputCore",
|
||||
"AIModule",
|
||||
"GameplayTasks",
|
||||
"NavigationSystem",
|
||||
});
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(new string[]
|
||||
{
|
||||
"DeveloperSettings",
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#include "BT/PS_AI_Behavior_BTDecorator_CheckTrait.h"
|
||||
#include "PS_AI_Behavior_AIController.h"
|
||||
#include "PS_AI_Behavior_PersonalityComponent.h"
|
||||
|
||||
UPS_AI_Behavior_BTDecorator_CheckTrait::UPS_AI_Behavior_BTDecorator_CheckTrait()
|
||||
{
|
||||
NodeName = TEXT("Check Trait");
|
||||
}
|
||||
|
||||
bool UPS_AI_Behavior_BTDecorator_CheckTrait::CalculateRawConditionValue(
|
||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const
|
||||
{
|
||||
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp.GetAIOwner());
|
||||
if (!AIC) return false;
|
||||
|
||||
UPS_AI_Behavior_PersonalityComponent* Personality = AIC->GetPersonalityComponent();
|
||||
if (!Personality) return false;
|
||||
|
||||
const float TraitValue = Personality->GetTrait(TraitAxis);
|
||||
|
||||
switch (Comparison)
|
||||
{
|
||||
case EPS_AI_Behavior_ComparisonOp::GreaterThan: return TraitValue > Threshold;
|
||||
case EPS_AI_Behavior_ComparisonOp::GreaterOrEqual: return TraitValue >= Threshold;
|
||||
case EPS_AI_Behavior_ComparisonOp::LessThan: return TraitValue < Threshold;
|
||||
case EPS_AI_Behavior_ComparisonOp::LessOrEqual: return TraitValue <= Threshold;
|
||||
case EPS_AI_Behavior_ComparisonOp::Equal: return FMath::IsNearlyEqual(TraitValue, Threshold, 0.01f);
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
FString UPS_AI_Behavior_BTDecorator_CheckTrait::GetStaticDescription() const
|
||||
{
|
||||
const UEnum* AxisEnum = StaticEnum<EPS_AI_Behavior_TraitAxis>();
|
||||
const UEnum* OpEnum = StaticEnum<EPS_AI_Behavior_ComparisonOp>();
|
||||
|
||||
return FString::Printf(TEXT("Trait: %s %s %.2f"),
|
||||
*AxisEnum->GetDisplayNameTextByValue(static_cast<int64>(TraitAxis)).ToString(),
|
||||
*OpEnum->GetDisplayNameTextByValue(static_cast<int64>(Comparison)).ToString(),
|
||||
Threshold);
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#include "BT/PS_AI_Behavior_BTService_EvaluateReaction.h"
|
||||
#include "PS_AI_Behavior_AIController.h"
|
||||
#include "PS_AI_Behavior_PersonalityComponent.h"
|
||||
#include "PS_AI_Behavior_Definitions.h"
|
||||
#include "BehaviorTree/BlackboardComponent.h"
|
||||
|
||||
UPS_AI_Behavior_BTService_EvaluateReaction::UPS_AI_Behavior_BTService_EvaluateReaction()
|
||||
{
|
||||
NodeName = TEXT("Evaluate Reaction");
|
||||
Interval = 0.5f;
|
||||
RandomDeviation = 0.1f;
|
||||
}
|
||||
|
||||
void UPS_AI_Behavior_BTService_EvaluateReaction::TickNode(
|
||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
|
||||
{
|
||||
Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);
|
||||
|
||||
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp.GetAIOwner());
|
||||
if (!AIC) return;
|
||||
|
||||
UPS_AI_Behavior_PersonalityComponent* Personality = AIC->GetPersonalityComponent();
|
||||
if (!Personality) return;
|
||||
|
||||
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
|
||||
if (!BB) return;
|
||||
|
||||
// Evaluate and apply the reaction
|
||||
const EPS_AI_Behavior_State NewState = Personality->ApplyReaction();
|
||||
|
||||
// Write to Blackboard
|
||||
AIC->SetBehaviorState(NewState);
|
||||
}
|
||||
|
||||
FString UPS_AI_Behavior_BTService_EvaluateReaction::GetStaticDescription() const
|
||||
{
|
||||
return TEXT("Evaluates NPC reaction from personality + threat.\nWrites: BehaviorState.");
|
||||
}
|
||||
@ -0,0 +1,70 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#include "BT/PS_AI_Behavior_BTService_UpdateThreat.h"
|
||||
#include "PS_AI_Behavior_AIController.h"
|
||||
#include "PS_AI_Behavior_PerceptionComponent.h"
|
||||
#include "PS_AI_Behavior_PersonalityComponent.h"
|
||||
#include "PS_AI_Behavior_Settings.h"
|
||||
#include "PS_AI_Behavior_Definitions.h"
|
||||
#include "BehaviorTree/BlackboardComponent.h"
|
||||
|
||||
UPS_AI_Behavior_BTService_UpdateThreat::UPS_AI_Behavior_BTService_UpdateThreat()
|
||||
{
|
||||
NodeName = TEXT("Update Threat");
|
||||
Interval = 0.3f;
|
||||
RandomDeviation = 0.05f;
|
||||
}
|
||||
|
||||
void UPS_AI_Behavior_BTService_UpdateThreat::TickNode(
|
||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
|
||||
{
|
||||
Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);
|
||||
|
||||
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp.GetAIOwner());
|
||||
if (!AIC) return;
|
||||
|
||||
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
|
||||
if (!BB) return;
|
||||
|
||||
UPS_AI_Behavior_PerceptionComponent* Perception = AIC->GetBehaviorPerception();
|
||||
if (!Perception) return;
|
||||
|
||||
// Calculate current threat
|
||||
const float RawThreat = Perception->CalculateThreatLevel();
|
||||
|
||||
// Get current stored threat for decay
|
||||
const float StoredThreat = BB->GetValueAsFloat(PS_AI_Behavior_BB::ThreatLevel);
|
||||
|
||||
// Apply decay when no threat, or take the max of new vs decayed
|
||||
const UPS_AI_Behavior_Settings* Settings = GetDefault<UPS_AI_Behavior_Settings>();
|
||||
const float DecayedThreat = FMath::Max(0.0f, StoredThreat - Settings->ThreatDecayRate * DeltaSeconds);
|
||||
const float FinalThreat = FMath::Max(RawThreat, DecayedThreat);
|
||||
|
||||
BB->SetValueAsFloat(PS_AI_Behavior_BB::ThreatLevel, FinalThreat);
|
||||
|
||||
// Update threat actor and location
|
||||
AActor* ThreatActor = Perception->GetHighestThreatActor();
|
||||
if (ThreatActor)
|
||||
{
|
||||
BB->SetValueAsObject(PS_AI_Behavior_BB::ThreatActor, ThreatActor);
|
||||
BB->SetValueAsVector(PS_AI_Behavior_BB::ThreatLocation, ThreatActor->GetActorLocation());
|
||||
}
|
||||
else if (FinalThreat <= 0.01f)
|
||||
{
|
||||
// Clear threat data when fully decayed
|
||||
BB->ClearValue(PS_AI_Behavior_BB::ThreatActor);
|
||||
BB->ClearValue(PS_AI_Behavior_BB::ThreatLocation);
|
||||
}
|
||||
|
||||
// Sync to PersonalityComponent
|
||||
UPS_AI_Behavior_PersonalityComponent* Personality = AIC->GetPersonalityComponent();
|
||||
if (Personality)
|
||||
{
|
||||
Personality->PerceivedThreatLevel = FinalThreat;
|
||||
}
|
||||
}
|
||||
|
||||
FString UPS_AI_Behavior_BTService_UpdateThreat::GetStaticDescription() const
|
||||
{
|
||||
return TEXT("Updates Blackboard threat data from perception.\nWrites: ThreatActor, ThreatLocation, ThreatLevel.");
|
||||
}
|
||||
@ -0,0 +1,147 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#include "BT/PS_AI_Behavior_BTTask_Attack.h"
|
||||
#include "PS_AI_Behavior_AIController.h"
|
||||
#include "PS_AI_Behavior_CombatComponent.h"
|
||||
#include "PS_AI_Behavior_Definitions.h"
|
||||
#include "BehaviorTree/BlackboardComponent.h"
|
||||
#include "Navigation/PathFollowingComponent.h"
|
||||
|
||||
UPS_AI_Behavior_BTTask_Attack::UPS_AI_Behavior_BTTask_Attack()
|
||||
{
|
||||
NodeName = TEXT("Attack");
|
||||
bNotifyTick = true;
|
||||
bNotifyTaskFinished = true;
|
||||
}
|
||||
|
||||
EBTNodeResult::Type UPS_AI_Behavior_BTTask_Attack::ExecuteTask(
|
||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
|
||||
{
|
||||
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp.GetAIOwner());
|
||||
if (!AIC || !AIC->GetPawn()) return EBTNodeResult::Failed;
|
||||
|
||||
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)
|
||||
{
|
||||
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 task: no CombatComponent on Pawn."), *AIC->GetName());
|
||||
return EBTNodeResult::Failed;
|
||||
}
|
||||
|
||||
// Try to attack immediately if in range
|
||||
if (Combat->IsInAttackRange(Target))
|
||||
{
|
||||
if (Combat->CanAttack())
|
||||
{
|
||||
Combat->ExecuteAttack(Target);
|
||||
return EBTNodeResult::Succeeded;
|
||||
}
|
||||
// In range but on cooldown — wait
|
||||
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)
|
||||
{
|
||||
return EBTNodeResult::Failed;
|
||||
}
|
||||
|
||||
FAttackMemory* Memory = reinterpret_cast<FAttackMemory*>(NodeMemory);
|
||||
Memory->bMovingToTarget = true;
|
||||
return EBTNodeResult::InProgress;
|
||||
}
|
||||
|
||||
void UPS_AI_Behavior_BTTask_Attack::TickTask(
|
||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
|
||||
{
|
||||
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp.GetAIOwner());
|
||||
if (!AIC || !AIC->GetPawn())
|
||||
{
|
||||
FinishLatentTask(OwnerComp, EBTNodeResult::Failed);
|
||||
return;
|
||||
}
|
||||
|
||||
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
|
||||
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);
|
||||
FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
|
||||
}
|
||||
// Else: wait for cooldown (stay InProgress)
|
||||
}
|
||||
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(
|
||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
|
||||
{
|
||||
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp.GetAIOwner());
|
||||
if (AIC)
|
||||
{
|
||||
AIC->StopMovement();
|
||||
}
|
||||
return EBTNodeResult::Aborted;
|
||||
}
|
||||
|
||||
FString UPS_AI_Behavior_BTTask_Attack::GetStaticDescription() const
|
||||
{
|
||||
return TEXT("Move to threat and attack via CombatComponent.");
|
||||
}
|
||||
@ -0,0 +1,152 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#include "BT/PS_AI_Behavior_BTTask_FindAndFollowSpline.h"
|
||||
#include "PS_AI_Behavior_AIController.h"
|
||||
#include "PS_AI_Behavior_SplineFollowerComponent.h"
|
||||
#include "PS_AI_Behavior_SplineNetwork.h"
|
||||
#include "PS_AI_Behavior_SplinePath.h"
|
||||
#include "PS_AI_Behavior_PersonalityComponent.h"
|
||||
#include "PS_AI_Behavior_Definitions.h"
|
||||
#include "Navigation/PathFollowingComponent.h"
|
||||
|
||||
UPS_AI_Behavior_BTTask_FindAndFollowSpline::UPS_AI_Behavior_BTTask_FindAndFollowSpline()
|
||||
{
|
||||
NodeName = TEXT("Find & Start Spline");
|
||||
bNotifyTick = true;
|
||||
bNotifyTaskFinished = true;
|
||||
}
|
||||
|
||||
EBTNodeResult::Type UPS_AI_Behavior_BTTask_FindAndFollowSpline::ExecuteTask(
|
||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
|
||||
{
|
||||
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp.GetAIOwner());
|
||||
if (!AIC || !AIC->GetPawn()) return EBTNodeResult::Failed;
|
||||
|
||||
UPS_AI_Behavior_SplineFollowerComponent* Follower =
|
||||
AIC->GetPawn()->FindComponentByClass<UPS_AI_Behavior_SplineFollowerComponent>();
|
||||
if (!Follower)
|
||||
{
|
||||
UE_LOG(LogPS_AI_Behavior, Warning,
|
||||
TEXT("[%s] FindAndFollowSpline: no SplineFollowerComponent."), *AIC->GetName());
|
||||
return EBTNodeResult::Failed;
|
||||
}
|
||||
|
||||
// Determine NPC type
|
||||
EPS_AI_Behavior_NPCType NPCType = EPS_AI_Behavior_NPCType::Civilian;
|
||||
UPS_AI_Behavior_PersonalityComponent* Personality = AIC->GetPersonalityComponent();
|
||||
if (Personality)
|
||||
{
|
||||
NPCType = Personality->GetNPCType();
|
||||
}
|
||||
|
||||
// Find closest spline
|
||||
UPS_AI_Behavior_SplineNetwork* Network =
|
||||
GetWorld()->GetSubsystem<UPS_AI_Behavior_SplineNetwork>();
|
||||
if (!Network)
|
||||
{
|
||||
UE_LOG(LogPS_AI_Behavior, Warning,
|
||||
TEXT("[%s] FindAndFollowSpline: SplineNetwork subsystem not available."), *AIC->GetName());
|
||||
return EBTNodeResult::Failed;
|
||||
}
|
||||
|
||||
APS_AI_Behavior_SplinePath* ClosestSpline = nullptr;
|
||||
float DistAlongSpline = 0.0f;
|
||||
|
||||
if (!Network->FindClosestSpline(
|
||||
AIC->GetPawn()->GetActorLocation(), NPCType, MaxSearchDistance,
|
||||
ClosestSpline, DistAlongSpline))
|
||||
{
|
||||
UE_LOG(LogPS_AI_Behavior, Verbose,
|
||||
TEXT("[%s] FindAndFollowSpline: no accessible spline within %.0fcm."),
|
||||
*AIC->GetName(), MaxSearchDistance);
|
||||
return EBTNodeResult::Failed;
|
||||
}
|
||||
|
||||
// Check if we need to walk to the spline first
|
||||
const FVector SplinePoint = ClosestSpline->GetWorldLocationAtDistance(DistAlongSpline);
|
||||
const float GapToSpline = FVector::Dist(AIC->GetPawn()->GetActorLocation(), SplinePoint);
|
||||
|
||||
if (bWalkToSpline && GapToSpline > AcceptanceRadius)
|
||||
{
|
||||
// Walk to spline first via NavMesh
|
||||
const EPathFollowingRequestResult::Type Result = AIC->MoveToLocation(
|
||||
SplinePoint, AcceptanceRadius, /*bStopOnOverlap=*/true,
|
||||
/*bUsePathfinding=*/true, /*bProjectDestinationToNavigation=*/true,
|
||||
/*bCanStrafe=*/false);
|
||||
|
||||
if (Result == EPathFollowingRequestResult::Failed)
|
||||
{
|
||||
// Can't reach via NavMesh — try starting anyway (snap)
|
||||
Follower->StartFollowingAtDistance(ClosestSpline, DistAlongSpline);
|
||||
return EBTNodeResult::Succeeded;
|
||||
}
|
||||
|
||||
if (Result == EPathFollowingRequestResult::AlreadyAtGoal)
|
||||
{
|
||||
Follower->StartFollowingAtDistance(ClosestSpline, DistAlongSpline);
|
||||
return EBTNodeResult::Succeeded;
|
||||
}
|
||||
|
||||
// Store the spline to connect to after reaching it
|
||||
Follower->CurrentSpline = ClosestSpline;
|
||||
Follower->CurrentDistance = DistAlongSpline;
|
||||
|
||||
FFindSplineMemory* Memory = reinterpret_cast<FFindSplineMemory*>(NodeMemory);
|
||||
Memory->bMovingToSpline = true;
|
||||
return EBTNodeResult::InProgress;
|
||||
}
|
||||
|
||||
// Close enough — start immediately
|
||||
Follower->StartFollowingAtDistance(ClosestSpline, DistAlongSpline);
|
||||
return EBTNodeResult::Succeeded;
|
||||
}
|
||||
|
||||
void UPS_AI_Behavior_BTTask_FindAndFollowSpline::TickTask(
|
||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
|
||||
{
|
||||
FFindSplineMemory* Memory = reinterpret_cast<FFindSplineMemory*>(NodeMemory);
|
||||
if (!Memory->bMovingToSpline) return;
|
||||
|
||||
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp.GetAIOwner());
|
||||
if (!AIC || !AIC->GetPawn())
|
||||
{
|
||||
FinishLatentTask(OwnerComp, EBTNodeResult::Failed);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we've reached the spline
|
||||
if (AIC->GetMoveStatus() == EPathFollowingStatus::Idle)
|
||||
{
|
||||
Memory->bMovingToSpline = false;
|
||||
|
||||
UPS_AI_Behavior_SplineFollowerComponent* Follower =
|
||||
AIC->GetPawn()->FindComponentByClass<UPS_AI_Behavior_SplineFollowerComponent>();
|
||||
if (Follower && Follower->CurrentSpline)
|
||||
{
|
||||
Follower->StartFollowingAtDistance(
|
||||
Follower->CurrentSpline, Follower->CurrentDistance);
|
||||
FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
|
||||
}
|
||||
else
|
||||
{
|
||||
FinishLatentTask(OwnerComp, EBTNodeResult::Failed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EBTNodeResult::Type UPS_AI_Behavior_BTTask_FindAndFollowSpline::AbortTask(
|
||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
|
||||
{
|
||||
AAIController* AIC = OwnerComp.GetAIOwner();
|
||||
if (AIC)
|
||||
{
|
||||
AIC->StopMovement();
|
||||
}
|
||||
return EBTNodeResult::Aborted;
|
||||
}
|
||||
|
||||
FString UPS_AI_Behavior_BTTask_FindAndFollowSpline::GetStaticDescription() const
|
||||
{
|
||||
return FString::Printf(TEXT("Find nearest spline (max %.0fcm) and start following%s"),
|
||||
MaxSearchDistance, bWalkToSpline ? TEXT(" (walk to)") : TEXT(""));
|
||||
}
|
||||
@ -0,0 +1,271 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#include "BT/PS_AI_Behavior_BTTask_FindCover.h"
|
||||
#include "PS_AI_Behavior_AIController.h"
|
||||
#include "PS_AI_Behavior_CoverPoint.h"
|
||||
#include "PS_AI_Behavior_PersonalityComponent.h"
|
||||
#include "PS_AI_Behavior_Definitions.h"
|
||||
#include "BehaviorTree/BlackboardComponent.h"
|
||||
#include "NavigationSystem.h"
|
||||
#include "Navigation/PathFollowingComponent.h"
|
||||
#include "CollisionQueryParams.h"
|
||||
#include "Engine/World.h"
|
||||
#include "EngineUtils.h"
|
||||
|
||||
UPS_AI_Behavior_BTTask_FindCover::UPS_AI_Behavior_BTTask_FindCover()
|
||||
{
|
||||
NodeName = TEXT("Find Cover");
|
||||
bNotifyTick = true;
|
||||
bNotifyTaskFinished = true;
|
||||
}
|
||||
|
||||
EBTNodeResult::Type UPS_AI_Behavior_BTTask_FindCover::ExecuteTask(
|
||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
|
||||
{
|
||||
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp.GetAIOwner());
|
||||
if (!AIC || !AIC->GetPawn()) return EBTNodeResult::Failed;
|
||||
|
||||
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
|
||||
if (!BB) return EBTNodeResult::Failed;
|
||||
|
||||
UWorld* World = GetWorld();
|
||||
if (!World) return EBTNodeResult::Failed;
|
||||
|
||||
const FVector NpcLoc = AIC->GetPawn()->GetActorLocation();
|
||||
const FVector ThreatLoc = BB->GetValueAsVector(PS_AI_Behavior_BB::ThreatLocation);
|
||||
|
||||
if (ThreatLoc.IsZero())
|
||||
{
|
||||
return EBTNodeResult::Failed;
|
||||
}
|
||||
|
||||
// Determine NPC type for accessibility filtering
|
||||
EPS_AI_Behavior_NPCType NPCType = EPS_AI_Behavior_NPCType::Any;
|
||||
UPS_AI_Behavior_PersonalityComponent* Personality = AIC->GetPersonalityComponent();
|
||||
if (Personality)
|
||||
{
|
||||
NPCType = Personality->GetNPCType();
|
||||
}
|
||||
|
||||
// ─── Phase 1: Search manual CoverPoints ─────────────────────────────
|
||||
float ManualScore = -1.0f;
|
||||
APS_AI_Behavior_CoverPoint* BestManualPoint =
|
||||
FindBestManualCoverPoint(World, NpcLoc, ThreatLoc, NPCType, ManualScore);
|
||||
|
||||
FVector BestCoverPos = FVector::ZeroVector;
|
||||
float BestScore = -1.0f;
|
||||
APS_AI_Behavior_CoverPoint* ChosenPoint = nullptr;
|
||||
|
||||
if (BestManualPoint)
|
||||
{
|
||||
BestCoverPos = BestManualPoint->GetActorLocation();
|
||||
BestScore = ManualScore + ManualPointBonus; // Bonus for manual placement
|
||||
ChosenPoint = BestManualPoint;
|
||||
}
|
||||
|
||||
// ─── Phase 2: Procedural fallback (if allowed) ──────────────────────
|
||||
if (!bUseManualPointsOnly)
|
||||
{
|
||||
UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(World);
|
||||
if (NavSys)
|
||||
{
|
||||
for (int32 i = 0; i < NumCandidates; ++i)
|
||||
{
|
||||
const float Angle = (360.0f / NumCandidates) * i;
|
||||
const float Dist = FMath::RandRange(SearchRadius * 0.3f, SearchRadius);
|
||||
const FVector Dir = FVector::ForwardVector.RotateAngleAxis(Angle, FVector::UpVector);
|
||||
const FVector Candidate = NpcLoc + Dir * Dist;
|
||||
|
||||
FNavLocation NavLoc;
|
||||
if (!NavSys->ProjectPointToNavigation(Candidate, NavLoc, FVector(300.0f, 300.0f, 200.0f)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const float Score = EvaluateCoverQuality(World, NavLoc.Location, ThreatLoc, NpcLoc);
|
||||
if (Score > BestScore)
|
||||
{
|
||||
BestScore = Score;
|
||||
BestCoverPos = NavLoc.Location;
|
||||
ChosenPoint = nullptr; // Procedural, no actor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (BestScore < 0.1f)
|
||||
{
|
||||
UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[%s] FindCover: no suitable cover found."), *AIC->GetName());
|
||||
return EBTNodeResult::Failed;
|
||||
}
|
||||
|
||||
// ─── Claim and write to Blackboard ──────────────────────────────────
|
||||
if (ChosenPoint)
|
||||
{
|
||||
ChosenPoint->Claim(AIC->GetPawn());
|
||||
BB->SetValueAsObject(PS_AI_Behavior_BB::CoverPoint, ChosenPoint);
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[%s] FindCover: using manual %s '%s' (score %.2f)"),
|
||||
*AIC->GetName(),
|
||||
ChosenPoint->PointType == EPS_AI_Behavior_CoverPointType::Cover ? TEXT("Cover") : TEXT("HidingSpot"),
|
||||
*ChosenPoint->GetName(), BestScore);
|
||||
}
|
||||
else
|
||||
{
|
||||
BB->ClearValue(PS_AI_Behavior_BB::CoverPoint);
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[%s] FindCover: using procedural cover (score %.2f)"),
|
||||
*AIC->GetName(), BestScore);
|
||||
}
|
||||
|
||||
BB->SetValueAsVector(PS_AI_Behavior_BB::CoverLocation, BestCoverPos);
|
||||
|
||||
// Navigate to cover
|
||||
const EPathFollowingRequestResult::Type Result = AIC->MoveToLocation(
|
||||
BestCoverPos, AcceptanceRadius, /*bStopOnOverlap=*/true,
|
||||
/*bUsePathfinding=*/true, /*bProjectDestinationToNavigation=*/true,
|
||||
/*bCanStrafe=*/false);
|
||||
|
||||
if (Result == EPathFollowingRequestResult::Failed)
|
||||
{
|
||||
return EBTNodeResult::Failed;
|
||||
}
|
||||
|
||||
if (Result == EPathFollowingRequestResult::AlreadyAtGoal)
|
||||
{
|
||||
return EBTNodeResult::Succeeded;
|
||||
}
|
||||
|
||||
FCoverMemory* Memory = reinterpret_cast<FCoverMemory*>(NodeMemory);
|
||||
Memory->bMoveRequested = true;
|
||||
return EBTNodeResult::InProgress;
|
||||
}
|
||||
|
||||
void UPS_AI_Behavior_BTTask_FindCover::TickTask(
|
||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
|
||||
{
|
||||
FCoverMemory* Memory = reinterpret_cast<FCoverMemory*>(NodeMemory);
|
||||
if (!Memory->bMoveRequested) return;
|
||||
|
||||
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp.GetAIOwner());
|
||||
if (!AIC) { FinishLatentTask(OwnerComp, EBTNodeResult::Failed); return; }
|
||||
|
||||
if (AIC->GetMoveStatus() == EPathFollowingStatus::Idle)
|
||||
{
|
||||
Memory->bMoveRequested = false;
|
||||
FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
|
||||
}
|
||||
}
|
||||
|
||||
EBTNodeResult::Type UPS_AI_Behavior_BTTask_FindCover::AbortTask(
|
||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
|
||||
{
|
||||
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp.GetAIOwner());
|
||||
if (AIC)
|
||||
{
|
||||
AIC->StopMovement();
|
||||
|
||||
// Release any claimed cover point
|
||||
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
|
||||
if (BB)
|
||||
{
|
||||
APS_AI_Behavior_CoverPoint* Point =
|
||||
Cast<APS_AI_Behavior_CoverPoint>(BB->GetValueAsObject(PS_AI_Behavior_BB::CoverPoint));
|
||||
if (Point)
|
||||
{
|
||||
Point->Release(AIC->GetPawn());
|
||||
BB->ClearValue(PS_AI_Behavior_BB::CoverPoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
return EBTNodeResult::Aborted;
|
||||
}
|
||||
|
||||
// ─── Manual CoverPoint Search ───────────────────────────────────────────────
|
||||
|
||||
APS_AI_Behavior_CoverPoint* UPS_AI_Behavior_BTTask_FindCover::FindBestManualCoverPoint(
|
||||
const UWorld* World, const FVector& NpcLoc, const FVector& ThreatLoc,
|
||||
EPS_AI_Behavior_NPCType NPCType, float& OutScore) const
|
||||
{
|
||||
APS_AI_Behavior_CoverPoint* BestPoint = nullptr;
|
||||
OutScore = -1.0f;
|
||||
|
||||
for (TActorIterator<APS_AI_Behavior_CoverPoint> It(const_cast<UWorld*>(World)); It; ++It)
|
||||
{
|
||||
APS_AI_Behavior_CoverPoint* Point = *It;
|
||||
if (!Point || !Point->bEnabled) continue;
|
||||
|
||||
// Type filter
|
||||
if (Point->PointType != CoverPointType) continue;
|
||||
|
||||
// NPC type accessibility
|
||||
if (!Point->IsAccessibleTo(NPCType)) continue;
|
||||
|
||||
// Availability
|
||||
if (!Point->HasRoom()) continue;
|
||||
|
||||
// Distance check
|
||||
const float Dist = FVector::Dist(NpcLoc, Point->GetActorLocation());
|
||||
if (Dist > SearchRadius) continue;
|
||||
|
||||
// Evaluate quality against current threat
|
||||
float Score = Point->EvaluateAgainstThreat(ThreatLoc);
|
||||
|
||||
// Distance bonus — closer to NPC is better
|
||||
Score += FMath::GetMappedRangeValueClamped(
|
||||
FVector2D(0.0f, SearchRadius), FVector2D(0.15f, 0.0f), Dist);
|
||||
|
||||
if (Score > OutScore)
|
||||
{
|
||||
OutScore = Score;
|
||||
BestPoint = Point;
|
||||
}
|
||||
}
|
||||
|
||||
return BestPoint;
|
||||
}
|
||||
|
||||
// ─── Procedural Cover Quality ───────────────────────────────────────────────
|
||||
|
||||
float UPS_AI_Behavior_BTTask_FindCover::EvaluateCoverQuality(
|
||||
const UWorld* World, const FVector& CandidatePos,
|
||||
const FVector& ThreatLoc, const FVector& NpcLoc) const
|
||||
{
|
||||
float Score = 0.0f;
|
||||
|
||||
const FVector TraceStart = CandidatePos + FVector(0, 0, MinCoverHeight);
|
||||
const FVector TraceEnd = ThreatLoc + FVector(0, 0, 100.0f);
|
||||
|
||||
FHitResult Hit;
|
||||
FCollisionQueryParams Params(SCENE_QUERY_STAT(CoverCheck), true);
|
||||
|
||||
if (World->LineTraceSingleByChannel(Hit, TraceStart, TraceEnd, ECC_Visibility, Params))
|
||||
{
|
||||
Score += 0.5f;
|
||||
}
|
||||
|
||||
const FVector TraceStartLow = CandidatePos + FVector(0, 0, MinCoverHeight * 0.5f);
|
||||
if (World->LineTraceSingleByChannel(Hit, TraceStartLow, TraceEnd, ECC_Visibility, Params))
|
||||
{
|
||||
Score += 0.15f;
|
||||
}
|
||||
|
||||
const float DistFromThreat = FVector::Dist(CandidatePos, ThreatLoc);
|
||||
const float DistFromNpc = FVector::Dist(CandidatePos, NpcLoc);
|
||||
|
||||
Score += FMath::GetMappedRangeValueClamped(
|
||||
FVector2D(300.0f, 1500.0f), FVector2D(0.0f, 0.2f), DistFromThreat);
|
||||
Score += FMath::GetMappedRangeValueClamped(
|
||||
FVector2D(0.0f, SearchRadius), FVector2D(0.15f, 0.0f), DistFromNpc);
|
||||
|
||||
return Score;
|
||||
}
|
||||
|
||||
FString UPS_AI_Behavior_BTTask_FindCover::GetStaticDescription() const
|
||||
{
|
||||
return FString::Printf(TEXT("Find cover within %.0fcm\nManual %s + Procedural (%d candidates)\nBonus: +%.0f%%"),
|
||||
SearchRadius,
|
||||
CoverPointType == EPS_AI_Behavior_CoverPointType::Cover ? TEXT("Cover") : TEXT("Hiding"),
|
||||
NumCandidates,
|
||||
ManualPointBonus * 100.0f);
|
||||
}
|
||||
@ -0,0 +1,135 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#include "BT/PS_AI_Behavior_BTTask_FleeFrom.h"
|
||||
#include "PS_AI_Behavior_AIController.h"
|
||||
#include "PS_AI_Behavior_Definitions.h"
|
||||
#include "BehaviorTree/BlackboardComponent.h"
|
||||
#include "NavigationSystem.h"
|
||||
#include "Navigation/PathFollowingComponent.h"
|
||||
|
||||
UPS_AI_Behavior_BTTask_FleeFrom::UPS_AI_Behavior_BTTask_FleeFrom()
|
||||
{
|
||||
NodeName = TEXT("Flee From Threat");
|
||||
bNotifyTick = true;
|
||||
bNotifyTaskFinished = true;
|
||||
}
|
||||
|
||||
EBTNodeResult::Type UPS_AI_Behavior_BTTask_FleeFrom::ExecuteTask(
|
||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
|
||||
{
|
||||
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp.GetAIOwner());
|
||||
if (!AIC || !AIC->GetPawn()) return EBTNodeResult::Failed;
|
||||
|
||||
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
|
||||
if (!BB) return EBTNodeResult::Failed;
|
||||
|
||||
const FVector ThreatLoc = BB->GetValueAsVector(PS_AI_Behavior_BB::ThreatLocation);
|
||||
const FVector Origin = AIC->GetPawn()->GetActorLocation();
|
||||
|
||||
// If no valid threat location, fail
|
||||
if (ThreatLoc.IsZero())
|
||||
{
|
||||
return EBTNodeResult::Failed;
|
||||
}
|
||||
|
||||
FVector FleePoint;
|
||||
if (!FindFleePoint(Origin, ThreatLoc, FleePoint))
|
||||
{
|
||||
UE_LOG(LogPS_AI_Behavior, Warning, TEXT("[%s] Flee: could not find valid flee point."), *AIC->GetName());
|
||||
return EBTNodeResult::Failed;
|
||||
}
|
||||
|
||||
const EPathFollowingRequestResult::Type Result = AIC->MoveToLocation(
|
||||
FleePoint, AcceptanceRadius, /*bStopOnOverlap=*/true,
|
||||
/*bUsePathfinding=*/true, /*bProjectDestinationToNavigation=*/true,
|
||||
/*bCanStrafe=*/false);
|
||||
|
||||
if (Result == EPathFollowingRequestResult::Failed)
|
||||
{
|
||||
return EBTNodeResult::Failed;
|
||||
}
|
||||
|
||||
if (Result == EPathFollowingRequestResult::AlreadyAtGoal)
|
||||
{
|
||||
return EBTNodeResult::Succeeded;
|
||||
}
|
||||
|
||||
FFleeMemory* Memory = reinterpret_cast<FFleeMemory*>(NodeMemory);
|
||||
Memory->bMoveRequested = true;
|
||||
return EBTNodeResult::InProgress;
|
||||
}
|
||||
|
||||
void UPS_AI_Behavior_BTTask_FleeFrom::TickTask(
|
||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
|
||||
{
|
||||
FFleeMemory* Memory = reinterpret_cast<FFleeMemory*>(NodeMemory);
|
||||
if (!Memory->bMoveRequested) return;
|
||||
|
||||
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp.GetAIOwner());
|
||||
if (!AIC) { FinishLatentTask(OwnerComp, EBTNodeResult::Failed); return; }
|
||||
|
||||
if (AIC->GetMoveStatus() == EPathFollowingStatus::Idle)
|
||||
{
|
||||
Memory->bMoveRequested = false;
|
||||
FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
|
||||
}
|
||||
}
|
||||
|
||||
EBTNodeResult::Type UPS_AI_Behavior_BTTask_FleeFrom::AbortTask(
|
||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
|
||||
{
|
||||
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp.GetAIOwner());
|
||||
if (AIC)
|
||||
{
|
||||
AIC->StopMovement();
|
||||
}
|
||||
return EBTNodeResult::Aborted;
|
||||
}
|
||||
|
||||
bool UPS_AI_Behavior_BTTask_FleeFrom::FindFleePoint(
|
||||
const FVector& Origin, const FVector& ThreatLoc, FVector& OutFleePoint) const
|
||||
{
|
||||
UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(GetWorld());
|
||||
if (!NavSys) return false;
|
||||
|
||||
// Direction away from threat
|
||||
FVector FleeDir = (Origin - ThreatLoc).GetSafeNormal2D();
|
||||
if (FleeDir.IsNearlyZero())
|
||||
{
|
||||
FleeDir = FVector::ForwardVector; // Fallback
|
||||
}
|
||||
|
||||
// Try multiple angles to find a valid navmesh point
|
||||
const int32 NumAttempts = 8;
|
||||
const float AngleStep = 45.0f;
|
||||
float BestDistFromThreat = 0.0f;
|
||||
|
||||
for (int32 i = 0; i < NumAttempts; ++i)
|
||||
{
|
||||
// Spread from directly away, rotating by increments
|
||||
const float Angle = (i % 2 == 0 ? 1.0f : -1.0f) * (i / 2) * AngleStep;
|
||||
const FVector RotatedDir = FleeDir.RotateAngleAxis(Angle, FVector::UpVector);
|
||||
const float FleeDist = FMath::RandRange(MinFleeDistance, MaxFleeDistance);
|
||||
const FVector CandidatePoint = Origin + RotatedDir * FleeDist;
|
||||
|
||||
// Project onto NavMesh
|
||||
FNavLocation NavLoc;
|
||||
if (NavSys->ProjectPointToNavigation(CandidatePoint, NavLoc, FVector(500.0f, 500.0f, 250.0f)))
|
||||
{
|
||||
const float DistFromThreat = FVector::Dist(NavLoc.Location, ThreatLoc);
|
||||
if (DistFromThreat > BestDistFromThreat)
|
||||
{
|
||||
BestDistFromThreat = DistFromThreat;
|
||||
OutFleePoint = NavLoc.Location;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return BestDistFromThreat > 0.0f;
|
||||
}
|
||||
|
||||
FString UPS_AI_Behavior_BTTask_FleeFrom::GetStaticDescription() const
|
||||
{
|
||||
return FString::Printf(TEXT("Flee %.0f-%.0fcm from threat"),
|
||||
MinFleeDistance, MaxFleeDistance);
|
||||
}
|
||||
@ -0,0 +1,127 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#include "BT/PS_AI_Behavior_BTTask_FollowSpline.h"
|
||||
#include "PS_AI_Behavior_AIController.h"
|
||||
#include "PS_AI_Behavior_SplineFollowerComponent.h"
|
||||
#include "PS_AI_Behavior_SplinePath.h"
|
||||
#include "PS_AI_Behavior_Definitions.h"
|
||||
|
||||
UPS_AI_Behavior_BTTask_FollowSpline::UPS_AI_Behavior_BTTask_FollowSpline()
|
||||
{
|
||||
NodeName = TEXT("Follow Spline");
|
||||
bNotifyTick = true;
|
||||
bNotifyTaskFinished = true;
|
||||
}
|
||||
|
||||
EBTNodeResult::Type UPS_AI_Behavior_BTTask_FollowSpline::ExecuteTask(
|
||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
|
||||
{
|
||||
AAIController* AIC = OwnerComp.GetAIOwner();
|
||||
if (!AIC || !AIC->GetPawn()) return EBTNodeResult::Failed;
|
||||
|
||||
UPS_AI_Behavior_SplineFollowerComponent* Follower =
|
||||
AIC->GetPawn()->FindComponentByClass<UPS_AI_Behavior_SplineFollowerComponent>();
|
||||
if (!Follower) return EBTNodeResult::Failed;
|
||||
|
||||
if (!Follower->CurrentSpline)
|
||||
{
|
||||
UE_LOG(LogPS_AI_Behavior, Warning, TEXT("[%s] FollowSpline: no current spline set."),
|
||||
*AIC->GetName());
|
||||
return EBTNodeResult::Failed;
|
||||
}
|
||||
|
||||
// Optional random direction
|
||||
if (bRandomDirection)
|
||||
{
|
||||
Follower->bMovingForward = FMath::RandBool();
|
||||
}
|
||||
|
||||
// Start or resume following
|
||||
if (!Follower->bIsFollowing)
|
||||
{
|
||||
Follower->ResumeFollowing();
|
||||
}
|
||||
|
||||
// Initialize memory — TickTask will poll bIsFollowing to detect end-of-spline
|
||||
FFollowMemory* Memory = reinterpret_cast<FFollowMemory*>(NodeMemory);
|
||||
Memory->Elapsed = 0.0f;
|
||||
Memory->bEndReached = false;
|
||||
|
||||
return EBTNodeResult::InProgress;
|
||||
}
|
||||
|
||||
void UPS_AI_Behavior_BTTask_FollowSpline::TickTask(
|
||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
|
||||
{
|
||||
FFollowMemory* Memory = reinterpret_cast<FFollowMemory*>(NodeMemory);
|
||||
|
||||
// Check if spline end was reached (poll bIsFollowing — set to false by SplineFollowerComponent)
|
||||
AAIController* AICCheck = OwnerComp.GetAIOwner();
|
||||
if (AICCheck && AICCheck->GetPawn())
|
||||
{
|
||||
UPS_AI_Behavior_SplineFollowerComponent* FollowerCheck =
|
||||
AICCheck->GetPawn()->FindComponentByClass<UPS_AI_Behavior_SplineFollowerComponent>();
|
||||
if (FollowerCheck && !FollowerCheck->bIsFollowing)
|
||||
{
|
||||
FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Time limit check
|
||||
if (MaxFollowTime > 0.0f)
|
||||
{
|
||||
Memory->Elapsed += DeltaSeconds;
|
||||
if (Memory->Elapsed >= MaxFollowTime)
|
||||
{
|
||||
// Pause following (don't stop — can resume later)
|
||||
AAIController* AIC = OwnerComp.GetAIOwner();
|
||||
if (AIC && AIC->GetPawn())
|
||||
{
|
||||
UPS_AI_Behavior_SplineFollowerComponent* Follower =
|
||||
AIC->GetPawn()->FindComponentByClass<UPS_AI_Behavior_SplineFollowerComponent>();
|
||||
if (Follower)
|
||||
{
|
||||
Follower->PauseFollowing();
|
||||
}
|
||||
}
|
||||
FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Verify pawn is still valid
|
||||
AAIController* AIC = OwnerComp.GetAIOwner();
|
||||
if (!AIC || !AIC->GetPawn())
|
||||
{
|
||||
FinishLatentTask(OwnerComp, EBTNodeResult::Failed);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
EBTNodeResult::Type UPS_AI_Behavior_BTTask_FollowSpline::AbortTask(
|
||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
|
||||
{
|
||||
AAIController* AIC = OwnerComp.GetAIOwner();
|
||||
if (AIC && AIC->GetPawn())
|
||||
{
|
||||
UPS_AI_Behavior_SplineFollowerComponent* Follower =
|
||||
AIC->GetPawn()->FindComponentByClass<UPS_AI_Behavior_SplineFollowerComponent>();
|
||||
if (Follower)
|
||||
{
|
||||
Follower->PauseFollowing();
|
||||
}
|
||||
}
|
||||
return EBTNodeResult::Aborted;
|
||||
}
|
||||
|
||||
FString UPS_AI_Behavior_BTTask_FollowSpline::GetStaticDescription() const
|
||||
{
|
||||
if (MaxFollowTime > 0.0f)
|
||||
{
|
||||
return FString::Printf(TEXT("Follow current spline (max %.1fs%s)"),
|
||||
MaxFollowTime, bRandomDirection ? TEXT(", random dir") : TEXT(""));
|
||||
}
|
||||
return FString::Printf(TEXT("Follow current spline%s"),
|
||||
bRandomDirection ? TEXT(" (random dir)") : TEXT(""));
|
||||
}
|
||||
@ -0,0 +1,130 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#include "BT/PS_AI_Behavior_BTTask_Patrol.h"
|
||||
#include "PS_AI_Behavior_AIController.h"
|
||||
#include "PS_AI_Behavior_Definitions.h"
|
||||
#include "BehaviorTree/BlackboardComponent.h"
|
||||
#include "Navigation/PathFollowingComponent.h"
|
||||
|
||||
UPS_AI_Behavior_BTTask_Patrol::UPS_AI_Behavior_BTTask_Patrol()
|
||||
{
|
||||
NodeName = TEXT("Patrol");
|
||||
bNotifyTick = true;
|
||||
bNotifyTaskFinished = true;
|
||||
}
|
||||
|
||||
EBTNodeResult::Type UPS_AI_Behavior_BTTask_Patrol::ExecuteTask(
|
||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
|
||||
{
|
||||
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp.GetAIOwner());
|
||||
if (!AIC) return EBTNodeResult::Failed;
|
||||
|
||||
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
|
||||
if (!BB) return EBTNodeResult::Failed;
|
||||
|
||||
// Check we have patrol points
|
||||
if (AIC->PatrolPoints.Num() == 0)
|
||||
{
|
||||
UE_LOG(LogPS_AI_Behavior, Warning, TEXT("[%s] Patrol task: no patrol points defined."), *AIC->GetName());
|
||||
return EBTNodeResult::Failed;
|
||||
}
|
||||
|
||||
FPatrolMemory* Memory = reinterpret_cast<FPatrolMemory*>(NodeMemory);
|
||||
Memory->bIsWaiting = false;
|
||||
Memory->bMoveRequested = false;
|
||||
Memory->WaitRemaining = 0.0f;
|
||||
|
||||
// Get current patrol index
|
||||
const int32 PatrolIdx = BB->GetValueAsInt(PS_AI_Behavior_BB::PatrolIndex);
|
||||
const int32 SafeIdx = PatrolIdx % AIC->PatrolPoints.Num();
|
||||
const FVector Destination = AIC->PatrolPoints[SafeIdx];
|
||||
|
||||
// Issue move request
|
||||
const EPathFollowingRequestResult::Type Result = AIC->MoveToLocation(
|
||||
Destination, AcceptanceRadius, /*bStopOnOverlap=*/true,
|
||||
/*bUsePathfinding=*/true, /*bProjectDestinationToNavigation=*/true,
|
||||
/*bCanStrafe=*/false);
|
||||
|
||||
if (Result == EPathFollowingRequestResult::Failed)
|
||||
{
|
||||
UE_LOG(LogPS_AI_Behavior, Warning, TEXT("[%s] Patrol: MoveTo failed for point %d."),
|
||||
*AIC->GetName(), SafeIdx);
|
||||
return EBTNodeResult::Failed;
|
||||
}
|
||||
|
||||
if (Result == EPathFollowingRequestResult::AlreadyAtGoal)
|
||||
{
|
||||
// Already there — start wait
|
||||
Memory->bIsWaiting = true;
|
||||
Memory->WaitRemaining = FMath::RandRange(MinWaitTime, MaxWaitTime);
|
||||
|
||||
// Advance patrol index
|
||||
BB->SetValueAsInt(PS_AI_Behavior_BB::PatrolIndex, (SafeIdx + 1) % AIC->PatrolPoints.Num());
|
||||
|
||||
return EBTNodeResult::InProgress;
|
||||
}
|
||||
|
||||
Memory->bMoveRequested = true;
|
||||
return EBTNodeResult::InProgress;
|
||||
}
|
||||
|
||||
void UPS_AI_Behavior_BTTask_Patrol::TickTask(
|
||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
|
||||
{
|
||||
FPatrolMemory* Memory = reinterpret_cast<FPatrolMemory*>(NodeMemory);
|
||||
|
||||
if (Memory->bIsWaiting)
|
||||
{
|
||||
Memory->WaitRemaining -= DeltaSeconds;
|
||||
if (Memory->WaitRemaining <= 0.0f)
|
||||
{
|
||||
FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (Memory->bMoveRequested)
|
||||
{
|
||||
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp.GetAIOwner());
|
||||
if (!AIC) { FinishLatentTask(OwnerComp, EBTNodeResult::Failed); return; }
|
||||
|
||||
const EPathFollowingStatus::Type MoveStatus = AIC->GetMoveStatus();
|
||||
|
||||
if (MoveStatus == EPathFollowingStatus::Idle)
|
||||
{
|
||||
// Move completed — start wait at waypoint
|
||||
Memory->bMoveRequested = false;
|
||||
Memory->bIsWaiting = true;
|
||||
Memory->WaitRemaining = FMath::RandRange(MinWaitTime, MaxWaitTime);
|
||||
|
||||
// Advance patrol index
|
||||
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
|
||||
if (BB)
|
||||
{
|
||||
const int32 PatrolIdx = BB->GetValueAsInt(PS_AI_Behavior_BB::PatrolIndex);
|
||||
const int32 NumPoints = AIC->PatrolPoints.Num();
|
||||
if (NumPoints > 0)
|
||||
{
|
||||
BB->SetValueAsInt(PS_AI_Behavior_BB::PatrolIndex, (PatrolIdx + 1) % NumPoints);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EBTNodeResult::Type UPS_AI_Behavior_BTTask_Patrol::AbortTask(
|
||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
|
||||
{
|
||||
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp.GetAIOwner());
|
||||
if (AIC)
|
||||
{
|
||||
AIC->StopMovement();
|
||||
}
|
||||
return EBTNodeResult::Aborted;
|
||||
}
|
||||
|
||||
FString UPS_AI_Behavior_BTTask_Patrol::GetStaticDescription() const
|
||||
{
|
||||
return FString::Printf(TEXT("Patrol (wait %.1f-%.1fs, radius %.0fcm)"),
|
||||
MinWaitTime, MaxWaitTime, AcceptanceRadius);
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#include "EQS/PS_AI_Behavior_EQSContext_Threat.h"
|
||||
#include "PS_AI_Behavior_AIController.h"
|
||||
#include "PS_AI_Behavior_Definitions.h"
|
||||
#include "EnvironmentQuery/EnvQueryTypes.h"
|
||||
#include "EnvironmentQuery/Items/EnvQueryItemType_Actor.h"
|
||||
#include "EnvironmentQuery/Items/EnvQueryItemType_Point.h"
|
||||
#include "BehaviorTree/BlackboardComponent.h"
|
||||
|
||||
void UPS_AI_Behavior_EQSContext_Threat::ProvideContext(
|
||||
FEnvQueryInstance& QueryInstance, FEnvQueryContextData& ContextData) const
|
||||
{
|
||||
// Get the querier's AIController
|
||||
const AActor* QuerierActor = Cast<AActor>(QueryInstance.Owner.Get());
|
||||
if (!QuerierActor) return;
|
||||
|
||||
const APawn* QuerierPawn = Cast<APawn>(QuerierActor);
|
||||
if (!QuerierPawn) return;
|
||||
|
||||
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(QuerierPawn->GetController());
|
||||
if (!AIC) return;
|
||||
|
||||
UBlackboardComponent* BB = AIC->GetBlackboardComponent();
|
||||
if (!BB) return;
|
||||
|
||||
// Try to provide the threat actor first
|
||||
AActor* ThreatActor = Cast<AActor>(BB->GetValueAsObject(PS_AI_Behavior_BB::ThreatActor));
|
||||
if (ThreatActor)
|
||||
{
|
||||
UEnvQueryItemType_Actor::SetContextHelper(ContextData, ThreatActor);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fall back to threat location
|
||||
const FVector ThreatLoc = BB->GetValueAsVector(PS_AI_Behavior_BB::ThreatLocation);
|
||||
if (!ThreatLoc.IsZero())
|
||||
{
|
||||
UEnvQueryItemType_Point::SetContextHelper(ContextData, ThreatLoc);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,86 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#include "EQS/PS_AI_Behavior_EQSGenerator_CoverPoints.h"
|
||||
#include "PS_AI_Behavior_CoverPoint.h"
|
||||
#include "PS_AI_Behavior_PersonalityComponent.h"
|
||||
#include "PS_AI_Behavior_Interface.h"
|
||||
#include "EnvironmentQuery/EnvQueryTypes.h"
|
||||
#include "EnvironmentQuery/Items/EnvQueryItemType_Actor.h"
|
||||
#include "EngineUtils.h"
|
||||
#include "GameFramework/Pawn.h"
|
||||
|
||||
UPS_AI_Behavior_EQSGenerator_CoverPoints::UPS_AI_Behavior_EQSGenerator_CoverPoints()
|
||||
{
|
||||
ItemType = UEnvQueryItemType_Actor::StaticClass();
|
||||
}
|
||||
|
||||
void UPS_AI_Behavior_EQSGenerator_CoverPoints::GenerateItems(FEnvQueryInstance& QueryInstance) const
|
||||
{
|
||||
const UObject* QueryOwner = QueryInstance.Owner.Get();
|
||||
if (!QueryOwner) return;
|
||||
|
||||
const AActor* QuerierActor = Cast<AActor>(QueryOwner);
|
||||
if (!QuerierActor) return;
|
||||
|
||||
const UWorld* World = QuerierActor->GetWorld();
|
||||
if (!World) return;
|
||||
|
||||
const FVector QuerierLoc = QuerierActor->GetActorLocation();
|
||||
|
||||
// Determine NPC type for accessibility check
|
||||
EPS_AI_Behavior_NPCType NPCType = EPS_AI_Behavior_NPCType::Any;
|
||||
const APawn* QuerierPawn = Cast<APawn>(QuerierActor);
|
||||
if (QuerierPawn)
|
||||
{
|
||||
if (QuerierPawn->Implements<UPS_AI_Behavior_Interface>())
|
||||
{
|
||||
NPCType = IPS_AI_Behavior_Interface::Execute_GetBehaviorNPCType(const_cast<APawn*>(QuerierPawn));
|
||||
}
|
||||
else if (const auto* PC = QuerierPawn->FindComponentByClass<UPS_AI_Behavior_PersonalityComponent>())
|
||||
{
|
||||
NPCType = PC->GetNPCType();
|
||||
}
|
||||
}
|
||||
|
||||
// Collect matching cover points
|
||||
TArray<AActor*> FoundPoints;
|
||||
|
||||
for (TActorIterator<APS_AI_Behavior_CoverPoint> It(const_cast<UWorld*>(World)); It; ++It)
|
||||
{
|
||||
APS_AI_Behavior_CoverPoint* Point = *It;
|
||||
if (!Point || !Point->bEnabled) continue;
|
||||
|
||||
// Type filter
|
||||
if (Point->PointType != PointTypeFilter) continue;
|
||||
|
||||
// NPC type accessibility
|
||||
if (!Point->IsAccessibleTo(NPCType)) continue;
|
||||
|
||||
// Availability
|
||||
if (bOnlyAvailable && !Point->HasRoom()) continue;
|
||||
|
||||
// Distance
|
||||
if (FVector::Dist(QuerierLoc, Point->GetActorLocation()) > MaxDistance) continue;
|
||||
|
||||
FoundPoints.Add(Point);
|
||||
}
|
||||
|
||||
// Add items to the query
|
||||
for (AActor* Point : FoundPoints)
|
||||
{
|
||||
QueryInstance.AddItemData<UEnvQueryItemType_Actor>(Point);
|
||||
}
|
||||
}
|
||||
|
||||
FText UPS_AI_Behavior_EQSGenerator_CoverPoints::GetDescriptionTitle() const
|
||||
{
|
||||
return FText::FromString(FString::Printf(TEXT("Cover Points (%s)"),
|
||||
PointTypeFilter == EPS_AI_Behavior_CoverPointType::Cover ? TEXT("Cover") : TEXT("Hiding")));
|
||||
}
|
||||
|
||||
FText UPS_AI_Behavior_EQSGenerator_CoverPoints::GetDescriptionDetails() const
|
||||
{
|
||||
return FText::FromString(FString::Printf(
|
||||
TEXT("Max dist: %.0f, Available only: %s"),
|
||||
MaxDistance, bOnlyAvailable ? TEXT("Yes") : TEXT("No")));
|
||||
}
|
||||
@ -0,0 +1,89 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#include "EQS/PS_AI_Behavior_EQSTest_CoverQuality.h"
|
||||
#include "EQS/PS_AI_Behavior_EQSContext_Threat.h"
|
||||
#include "EnvironmentQuery/EnvQueryTypes.h"
|
||||
#include "EnvironmentQuery/Items/EnvQueryItemType_VectorBase.h"
|
||||
#include "CollisionQueryParams.h"
|
||||
#include "Engine/World.h"
|
||||
|
||||
UPS_AI_Behavior_EQSTest_CoverQuality::UPS_AI_Behavior_EQSTest_CoverQuality()
|
||||
{
|
||||
Cost = EEnvTestCost::High; // Uses raycasts
|
||||
ValidItemType = UEnvQueryItemType_VectorBase::StaticClass();
|
||||
SetWorkOnFloatValues(true);
|
||||
}
|
||||
|
||||
void UPS_AI_Behavior_EQSTest_CoverQuality::RunTest(FEnvQueryInstance& QueryInstance) const
|
||||
{
|
||||
UObject* QueryOwner = QueryInstance.Owner.Get();
|
||||
if (!QueryOwner) return;
|
||||
|
||||
// Get threat locations from context
|
||||
TArray<FVector> ThreatLocations;
|
||||
if (!QueryInstance.PrepareContext(UPS_AI_Behavior_EQSContext_Threat::StaticClass(), ThreatLocations))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (ThreatLocations.Num() == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const FVector ThreatLoc = ThreatLocations[0];
|
||||
const UWorld* World = GEngine->GetWorldFromContextObject(QueryOwner, EGetWorldErrorMode::LogAndReturnNull);
|
||||
if (!World) return;
|
||||
|
||||
FCollisionQueryParams TraceParams(SCENE_QUERY_STAT(CoverQualityEQS), true);
|
||||
|
||||
// Compute height steps
|
||||
TArray<float> TraceHeights;
|
||||
if (NumTraceHeights == 1)
|
||||
{
|
||||
TraceHeights.Add((MinTraceHeight + MaxTraceHeight) * 0.5f);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int32 i = 0; i < NumTraceHeights; ++i)
|
||||
{
|
||||
const float Alpha = static_cast<float>(i) / (NumTraceHeights - 1);
|
||||
TraceHeights.Add(FMath::Lerp(MinTraceHeight, MaxTraceHeight, Alpha));
|
||||
}
|
||||
}
|
||||
|
||||
for (FEnvQueryInstance::ItemIterator It(this, QueryInstance); It; ++It)
|
||||
{
|
||||
const FVector CandidatePos = GetItemLocation(QueryInstance, It.GetIndex());
|
||||
float BlockedCount = 0.0f;
|
||||
|
||||
for (float Height : TraceHeights)
|
||||
{
|
||||
const FVector TraceStart = CandidatePos + FVector(0, 0, Height);
|
||||
const FVector TraceEnd = ThreatLoc + FVector(0, 0, 150.0f); // Approx eye height
|
||||
|
||||
FHitResult Hit;
|
||||
if (World->LineTraceSingleByChannel(Hit, TraceStart, TraceEnd,
|
||||
ECC_Visibility, TraceParams))
|
||||
{
|
||||
BlockedCount += 1.0f;
|
||||
}
|
||||
}
|
||||
|
||||
// Score: ratio of blocked traces (0.0 = fully exposed, 1.0 = fully covered)
|
||||
const float Score = BlockedCount / static_cast<float>(TraceHeights.Num());
|
||||
It.SetScore(TestPurpose, FilterType, Score, 0.0f, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
FText UPS_AI_Behavior_EQSTest_CoverQuality::GetDescriptionTitle() const
|
||||
{
|
||||
return FText::FromString(TEXT("Cover Quality (vs Threat)"));
|
||||
}
|
||||
|
||||
FText UPS_AI_Behavior_EQSTest_CoverQuality::GetDescriptionDetails() const
|
||||
{
|
||||
return FText::FromString(FString::Printf(
|
||||
TEXT("%d traces from %.0f to %.0fcm height"),
|
||||
NumTraceHeights, MinTraceHeight, MaxTraceHeight));
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#include "PS_AI_Behavior.h"
|
||||
#include "PS_AI_Behavior_Definitions.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "FPS_AI_BehaviorModule"
|
||||
|
||||
IMPLEMENT_MODULE(FPS_AI_BehaviorModule, PS_AI_Behavior)
|
||||
|
||||
void FPS_AI_BehaviorModule::StartupModule()
|
||||
{
|
||||
UE_LOG(LogPS_AI_Behavior, Log, TEXT("PS_AI_Behavior module started."));
|
||||
}
|
||||
|
||||
void FPS_AI_BehaviorModule::ShutdownModule()
|
||||
{
|
||||
UE_LOG(LogPS_AI_Behavior, Log, TEXT("PS_AI_Behavior module shut down."));
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
@ -0,0 +1,357 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#include "PS_AI_Behavior_AIController.h"
|
||||
#include "PS_AI_Behavior_Interface.h"
|
||||
#include "PS_AI_Behavior_PerceptionComponent.h"
|
||||
#include "PS_AI_Behavior_PersonalityComponent.h"
|
||||
#include "PS_AI_Behavior_PersonalityProfile.h"
|
||||
#include "BehaviorTree/BehaviorTree.h"
|
||||
#include "BehaviorTree/BlackboardComponent.h"
|
||||
#include "BehaviorTree/BlackboardData.h"
|
||||
#include "BehaviorTree/Blackboard/BlackboardKeyType_Enum.h"
|
||||
#include "BehaviorTree/Blackboard/BlackboardKeyType_Float.h"
|
||||
#include "BehaviorTree/Blackboard/BlackboardKeyType_Int.h"
|
||||
#include "BehaviorTree/Blackboard/BlackboardKeyType_Object.h"
|
||||
#include "BehaviorTree/Blackboard/BlackboardKeyType_Vector.h"
|
||||
|
||||
APS_AI_Behavior_AIController::APS_AI_Behavior_AIController()
|
||||
{
|
||||
// Create our perception component
|
||||
BehaviorPerception = CreateDefaultSubobject<UPS_AI_Behavior_PerceptionComponent>(TEXT("BehaviorPerception"));
|
||||
SetPerceptionComponent(*BehaviorPerception);
|
||||
}
|
||||
|
||||
void APS_AI_Behavior_AIController::OnPossess(APawn* InPawn)
|
||||
{
|
||||
Super::OnPossess(InPawn);
|
||||
|
||||
if (!InPawn)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Find PersonalityComponent on the pawn
|
||||
PersonalityComp = InPawn->FindComponentByClass<UPS_AI_Behavior_PersonalityComponent>();
|
||||
if (!PersonalityComp)
|
||||
{
|
||||
UE_LOG(LogPS_AI_Behavior, Warning,
|
||||
TEXT("[%s] No PersonalityComponent found on Pawn '%s' — using defaults."),
|
||||
*GetName(), *InPawn->GetName());
|
||||
}
|
||||
|
||||
// Auto-assign Team ID from IPS_AI_Behavior interface (preferred) or PersonalityComponent
|
||||
if (TeamId == FGenericTeamId::NoTeam)
|
||||
{
|
||||
EPS_AI_Behavior_NPCType NPCType = EPS_AI_Behavior_NPCType::Any;
|
||||
|
||||
if (InPawn->Implements<UPS_AI_Behavior_Interface>())
|
||||
{
|
||||
// Use the interface — the host project controls the storage
|
||||
NPCType = IPS_AI_Behavior_Interface::Execute_GetBehaviorNPCType(InPawn);
|
||||
|
||||
// Also check if the interface provides a specific TeamId
|
||||
const uint8 InterfaceTeamId = IPS_AI_Behavior_Interface::Execute_GetBehaviorTeamId(InPawn);
|
||||
if (InterfaceTeamId != FGenericTeamId::NoTeam)
|
||||
{
|
||||
TeamId = InterfaceTeamId;
|
||||
}
|
||||
}
|
||||
else if (PersonalityComp)
|
||||
{
|
||||
// Fallback: get from PersonalityProfile
|
||||
NPCType = PersonalityComp->GetNPCType();
|
||||
}
|
||||
|
||||
// If interface didn't set a specific TeamId, derive from NPCType
|
||||
if (TeamId == FGenericTeamId::NoTeam)
|
||||
{
|
||||
switch (NPCType)
|
||||
{
|
||||
case EPS_AI_Behavior_NPCType::Civilian:
|
||||
TeamId = 1;
|
||||
break;
|
||||
case EPS_AI_Behavior_NPCType::Enemy:
|
||||
// Check if infiltrated (hostile=false → disguised as civilian)
|
||||
if (InPawn->Implements<UPS_AI_Behavior_Interface>() &&
|
||||
!IPS_AI_Behavior_Interface::Execute_IsBehaviorHostile(InPawn))
|
||||
{
|
||||
TeamId = 1; // Disguised as Civilian
|
||||
}
|
||||
else
|
||||
{
|
||||
TeamId = 2;
|
||||
}
|
||||
break;
|
||||
case EPS_AI_Behavior_NPCType::Protector:
|
||||
TeamId = 3;
|
||||
break;
|
||||
default:
|
||||
TeamId = FGenericTeamId::NoTeam; // 255 → Neutral to everyone
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] Auto-assigned TeamId=%d from NPCType=%s"),
|
||||
*GetName(), TeamId, *UEnum::GetValueAsString(NPCType));
|
||||
}
|
||||
|
||||
SetupBlackboard();
|
||||
StartBehavior();
|
||||
TryBindConversationAgent();
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] Possessed Pawn '%s' — BT started, TeamId=%d."),
|
||||
*GetName(), *InPawn->GetName(), TeamId);
|
||||
}
|
||||
|
||||
void APS_AI_Behavior_AIController::OnUnPossess()
|
||||
{
|
||||
// Stop the behavior tree
|
||||
UBrainComponent* Brain = GetBrainComponent();
|
||||
if (Brain)
|
||||
{
|
||||
Brain->StopLogic(TEXT("Unpossessed"));
|
||||
}
|
||||
|
||||
PersonalityComp = nullptr;
|
||||
Super::OnUnPossess();
|
||||
}
|
||||
|
||||
void APS_AI_Behavior_AIController::SetupBlackboard()
|
||||
{
|
||||
// Create a runtime Blackboard Data if none is assigned
|
||||
if (!BlackboardAsset)
|
||||
{
|
||||
BlackboardAsset = NewObject<UBlackboardData>(this, TEXT("RuntimeBlackboardData"));
|
||||
|
||||
// State (stored as uint8 enum)
|
||||
FBlackboardEntry StateEntry;
|
||||
StateEntry.EntryName = PS_AI_Behavior_BB::State;
|
||||
StateEntry.KeyType = NewObject<UBlackboardKeyType_Enum>(BlackboardAsset);
|
||||
BlackboardAsset->Keys.Add(StateEntry);
|
||||
|
||||
// ThreatActor
|
||||
FBlackboardEntry ThreatActorEntry;
|
||||
ThreatActorEntry.EntryName = PS_AI_Behavior_BB::ThreatActor;
|
||||
ThreatActorEntry.KeyType = NewObject<UBlackboardKeyType_Object>(BlackboardAsset);
|
||||
Cast<UBlackboardKeyType_Object>(ThreatActorEntry.KeyType)->BaseClass = AActor::StaticClass();
|
||||
BlackboardAsset->Keys.Add(ThreatActorEntry);
|
||||
|
||||
// ThreatLocation
|
||||
FBlackboardEntry ThreatLocEntry;
|
||||
ThreatLocEntry.EntryName = PS_AI_Behavior_BB::ThreatLocation;
|
||||
ThreatLocEntry.KeyType = NewObject<UBlackboardKeyType_Vector>(BlackboardAsset);
|
||||
BlackboardAsset->Keys.Add(ThreatLocEntry);
|
||||
|
||||
// ThreatLevel
|
||||
FBlackboardEntry ThreatLevelEntry;
|
||||
ThreatLevelEntry.EntryName = PS_AI_Behavior_BB::ThreatLevel;
|
||||
ThreatLevelEntry.KeyType = NewObject<UBlackboardKeyType_Float>(BlackboardAsset);
|
||||
BlackboardAsset->Keys.Add(ThreatLevelEntry);
|
||||
|
||||
// CoverLocation
|
||||
FBlackboardEntry CoverEntry;
|
||||
CoverEntry.EntryName = PS_AI_Behavior_BB::CoverLocation;
|
||||
CoverEntry.KeyType = NewObject<UBlackboardKeyType_Vector>(BlackboardAsset);
|
||||
BlackboardAsset->Keys.Add(CoverEntry);
|
||||
|
||||
// CoverPoint (Object — APS_AI_Behavior_CoverPoint)
|
||||
FBlackboardEntry CoverPointEntry;
|
||||
CoverPointEntry.EntryName = PS_AI_Behavior_BB::CoverPoint;
|
||||
CoverPointEntry.KeyType = NewObject<UBlackboardKeyType_Object>(BlackboardAsset);
|
||||
Cast<UBlackboardKeyType_Object>(CoverPointEntry.KeyType)->BaseClass = AActor::StaticClass();
|
||||
BlackboardAsset->Keys.Add(CoverPointEntry);
|
||||
|
||||
// PatrolIndex
|
||||
FBlackboardEntry PatrolEntry;
|
||||
PatrolEntry.EntryName = PS_AI_Behavior_BB::PatrolIndex;
|
||||
PatrolEntry.KeyType = NewObject<UBlackboardKeyType_Int>(BlackboardAsset);
|
||||
BlackboardAsset->Keys.Add(PatrolEntry);
|
||||
|
||||
// HomeLocation
|
||||
FBlackboardEntry HomeEntry;
|
||||
HomeEntry.EntryName = PS_AI_Behavior_BB::HomeLocation;
|
||||
HomeEntry.KeyType = NewObject<UBlackboardKeyType_Vector>(BlackboardAsset);
|
||||
BlackboardAsset->Keys.Add(HomeEntry);
|
||||
|
||||
// CurrentSpline (Object — APS_AI_Behavior_SplinePath)
|
||||
FBlackboardEntry SplineEntry;
|
||||
SplineEntry.EntryName = PS_AI_Behavior_BB::CurrentSpline;
|
||||
SplineEntry.KeyType = NewObject<UBlackboardKeyType_Object>(BlackboardAsset);
|
||||
Cast<UBlackboardKeyType_Object>(SplineEntry.KeyType)->BaseClass = AActor::StaticClass();
|
||||
BlackboardAsset->Keys.Add(SplineEntry);
|
||||
|
||||
// SplineProgress (float 0-1)
|
||||
FBlackboardEntry SplineProgressEntry;
|
||||
SplineProgressEntry.EntryName = PS_AI_Behavior_BB::SplineProgress;
|
||||
SplineProgressEntry.KeyType = NewObject<UBlackboardKeyType_Float>(BlackboardAsset);
|
||||
BlackboardAsset->Keys.Add(SplineProgressEntry);
|
||||
}
|
||||
|
||||
UBlackboardComponent* RawBBComp = nullptr;
|
||||
UseBlackboard(BlackboardAsset, RawBBComp);
|
||||
Blackboard = RawBBComp;
|
||||
|
||||
// Initialize home location to pawn's spawn position
|
||||
if (Blackboard && GetPawn())
|
||||
{
|
||||
Blackboard->SetValueAsVector(PS_AI_Behavior_BB::HomeLocation, GetPawn()->GetActorLocation());
|
||||
Blackboard->SetValueAsInt(PS_AI_Behavior_BB::PatrolIndex, 0);
|
||||
Blackboard->SetValueAsEnum(PS_AI_Behavior_BB::State,
|
||||
static_cast<uint8>(EPS_AI_Behavior_State::Idle));
|
||||
}
|
||||
}
|
||||
|
||||
void APS_AI_Behavior_AIController::StartBehavior()
|
||||
{
|
||||
UBehaviorTree* BTToRun = BehaviorTreeAsset;
|
||||
|
||||
// Fallback: get from personality profile
|
||||
if (!BTToRun && PersonalityComp && PersonalityComp->Profile)
|
||||
{
|
||||
BTToRun = PersonalityComp->Profile->DefaultBehaviorTree.LoadSynchronous();
|
||||
}
|
||||
|
||||
if (BTToRun)
|
||||
{
|
||||
RunBehaviorTree(BTToRun);
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogPS_AI_Behavior, Warning,
|
||||
TEXT("[%s] No BehaviorTree assigned and none in PersonalityProfile — NPC will be inert."),
|
||||
*GetName());
|
||||
}
|
||||
}
|
||||
|
||||
void APS_AI_Behavior_AIController::SetBehaviorState(EPS_AI_Behavior_State NewState)
|
||||
{
|
||||
if (Blackboard)
|
||||
{
|
||||
Blackboard->SetValueAsEnum(PS_AI_Behavior_BB::State, static_cast<uint8>(NewState));
|
||||
}
|
||||
}
|
||||
|
||||
EPS_AI_Behavior_State APS_AI_Behavior_AIController::GetBehaviorState() const
|
||||
{
|
||||
if (Blackboard)
|
||||
{
|
||||
return static_cast<EPS_AI_Behavior_State>(
|
||||
Blackboard->GetValueAsEnum(PS_AI_Behavior_BB::State));
|
||||
}
|
||||
return EPS_AI_Behavior_State::Idle;
|
||||
}
|
||||
|
||||
// ─── Team / Affiliation ─────────────────────────────────────────────────────
|
||||
|
||||
void APS_AI_Behavior_AIController::SetTeamId(uint8 NewTeamId)
|
||||
{
|
||||
TeamId = NewTeamId;
|
||||
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] TeamId set to %d"), *GetName(), TeamId);
|
||||
}
|
||||
|
||||
FGenericTeamId APS_AI_Behavior_AIController::GetGenericTeamId() const
|
||||
{
|
||||
return FGenericTeamId(TeamId);
|
||||
}
|
||||
|
||||
void APS_AI_Behavior_AIController::SetGenericTeamId(const FGenericTeamId& InTeamId)
|
||||
{
|
||||
TeamId = InTeamId.GetId();
|
||||
}
|
||||
|
||||
ETeamAttitude::Type APS_AI_Behavior_AIController::GetTeamAttitudeTowards(const AActor& Other) const
|
||||
{
|
||||
const uint8 OtherTeamId = FGenericTeamId::NoTeam;
|
||||
|
||||
// Try to get the other actor's team ID
|
||||
const APawn* OtherPawn = Cast<APawn>(&Other);
|
||||
if (!OtherPawn)
|
||||
{
|
||||
OtherPawn = Cast<APawn>(Other.GetInstigator());
|
||||
}
|
||||
|
||||
uint8 OtherTeam = FGenericTeamId::NoTeam;
|
||||
|
||||
if (OtherPawn)
|
||||
{
|
||||
// Check via AIController first
|
||||
if (const AAIController* OtherAIC = Cast<AAIController>(OtherPawn->GetController()))
|
||||
{
|
||||
OtherTeam = OtherAIC->GetGenericTeamId().GetId();
|
||||
}
|
||||
// Check via IPS_AI_Behavior interface
|
||||
else if (OtherPawn->Implements<UPS_AI_Behavior_Interface>())
|
||||
{
|
||||
OtherTeam = IPS_AI_Behavior_Interface::Execute_GetBehaviorTeamId(const_cast<APawn*>(OtherPawn));
|
||||
}
|
||||
}
|
||||
|
||||
// NoTeam (255) → Neutral
|
||||
if (TeamId == FGenericTeamId::NoTeam || OtherTeam == FGenericTeamId::NoTeam)
|
||||
{
|
||||
return ETeamAttitude::Neutral;
|
||||
}
|
||||
|
||||
// Same team → Friendly
|
||||
if (TeamId == OtherTeam)
|
||||
{
|
||||
return ETeamAttitude::Friendly;
|
||||
}
|
||||
|
||||
// ─── Custom cross-team attitudes ────────────────────────────────────
|
||||
|
||||
// Civilian (1) ↔ Protector (3) → Friendly
|
||||
if ((TeamId == 1 && OtherTeam == 3) || (TeamId == 3 && OtherTeam == 1))
|
||||
{
|
||||
return ETeamAttitude::Friendly;
|
||||
}
|
||||
|
||||
// Everything else → Hostile
|
||||
return ETeamAttitude::Hostile;
|
||||
}
|
||||
|
||||
// ─── ConvAgent Integration ──────────────────────────────────────────────────
|
||||
|
||||
void APS_AI_Behavior_AIController::TryBindConversationAgent()
|
||||
{
|
||||
APawn* MyPawn = GetPawn();
|
||||
if (!MyPawn) return;
|
||||
|
||||
// Soft lookup via reflection — no compile dependency on PS_AI_ConvAgent
|
||||
static UClass* ConvAgentClass = nullptr;
|
||||
if (!ConvAgentClass)
|
||||
{
|
||||
ConvAgentClass = LoadClass<UActorComponent>(nullptr,
|
||||
TEXT("/Script/PS_AI_ConvAgent.PS_AI_ConvAgent_ElevenLabsComponent"));
|
||||
}
|
||||
|
||||
if (!ConvAgentClass)
|
||||
{
|
||||
// PS_AI_ConvAgent plugin not loaded — that's fine
|
||||
return;
|
||||
}
|
||||
|
||||
UActorComponent* ConvComp = MyPawn->FindComponentByClass(ConvAgentClass);
|
||||
if (!ConvComp)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Log,
|
||||
TEXT("[%s] Found PS_AI_ConvAgent_ElevenLabsComponent on Pawn — binding for conversation-driven actions."),
|
||||
*GetName());
|
||||
|
||||
// Bind to OnAgentActionRequested delegate via reflection
|
||||
FMulticastDelegateProperty* ActionDelegate = CastField<FMulticastDelegateProperty>(
|
||||
ConvAgentClass->FindPropertyByName(TEXT("OnAgentActionRequested")));
|
||||
|
||||
if (ActionDelegate)
|
||||
{
|
||||
// The delegate binding would inject conversation-driven state changes
|
||||
// into the behavior tree via Blackboard writes.
|
||||
// Full implementation depends on PS_AI_ConvAgent's delegate signature.
|
||||
UE_LOG(LogPS_AI_Behavior, Log,
|
||||
TEXT("[%s] OnAgentActionRequested delegate found — conversation actions can drive behavior."),
|
||||
*GetName());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,115 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#include "PS_AI_Behavior_CombatComponent.h"
|
||||
#include "PS_AI_Behavior_Definitions.h"
|
||||
#include "GameFramework/DamageType.h"
|
||||
#include "Engine/DamageEvents.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "Net/UnrealNetwork.h"
|
||||
|
||||
UPS_AI_Behavior_CombatComponent::UPS_AI_Behavior_CombatComponent()
|
||||
{
|
||||
PrimaryComponentTick.bCanEverTick = true;
|
||||
PrimaryComponentTick.TickInterval = 0.1f; // Don't need per-frame
|
||||
SetIsReplicatedByDefault(true);
|
||||
}
|
||||
|
||||
void UPS_AI_Behavior_CombatComponent::GetLifetimeReplicatedProps(
|
||||
TArray<FLifetimeProperty>& OutLifetimeProps) const
|
||||
{
|
||||
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
|
||||
|
||||
DOREPLIFETIME(UPS_AI_Behavior_CombatComponent, CurrentTarget);
|
||||
}
|
||||
|
||||
void UPS_AI_Behavior_CombatComponent::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
if (!DamageTypeClass)
|
||||
{
|
||||
DamageTypeClass = UDamageType::StaticClass();
|
||||
}
|
||||
}
|
||||
|
||||
void UPS_AI_Behavior_CombatComponent::TickComponent(
|
||||
float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
|
||||
{
|
||||
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
|
||||
|
||||
// Cooldown only ticks on server (BT runs server-only)
|
||||
if (GetOwner() && GetOwner()->HasAuthority() && CooldownRemaining > 0.0f)
|
||||
{
|
||||
CooldownRemaining = FMath::Max(0.0f, CooldownRemaining - DeltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
bool UPS_AI_Behavior_CombatComponent::CanAttack() const
|
||||
{
|
||||
return CooldownRemaining <= 0.0f;
|
||||
}
|
||||
|
||||
bool UPS_AI_Behavior_CombatComponent::IsInAttackRange(AActor* Target) const
|
||||
{
|
||||
if (!Target || !GetOwner())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const float Dist = FVector::Dist(GetOwner()->GetActorLocation(), Target->GetActorLocation());
|
||||
return Dist <= AttackRange;
|
||||
}
|
||||
|
||||
bool UPS_AI_Behavior_CombatComponent::ExecuteAttack(AActor* Target)
|
||||
{
|
||||
// Only execute on server (authority)
|
||||
if (!GetOwner() || !GetOwner()->HasAuthority())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!CanAttack() || !Target)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!IsInAttackRange(Target))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Apply damage (server-only — UE5 damage system is server-authoritative)
|
||||
UGameplayStatics::ApplyDamage(
|
||||
Target,
|
||||
AttackDamage,
|
||||
GetOwner()->GetInstigatorController(),
|
||||
GetOwner(),
|
||||
DamageTypeClass);
|
||||
|
||||
// Start cooldown
|
||||
CooldownRemaining = AttackCooldown;
|
||||
CurrentTarget = Target; // Replicated → clients see the target
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[%s] Attacked '%s' for %.1f damage."),
|
||||
*GetOwner()->GetName(), *Target->GetName(), AttackDamage);
|
||||
|
||||
// Multicast cosmetic event to all clients (VFX, sound, anims)
|
||||
Multicast_OnAttackExecuted(Target);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void UPS_AI_Behavior_CombatComponent::Multicast_OnAttackExecuted_Implementation(AActor* Target)
|
||||
{
|
||||
// Fires on server AND all clients
|
||||
OnAttackExecuted.Broadcast(Target);
|
||||
}
|
||||
|
||||
void UPS_AI_Behavior_CombatComponent::NotifyDamageReceived(float Damage, AActor* DamageInstigator)
|
||||
{
|
||||
UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[%s] Received %.1f damage from '%s'."),
|
||||
*GetOwner()->GetName(), Damage,
|
||||
DamageInstigator ? *DamageInstigator->GetName() : TEXT("Unknown"));
|
||||
|
||||
OnDamageReceived.Broadcast(Damage, DamageInstigator);
|
||||
}
|
||||
@ -0,0 +1,194 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#include "PS_AI_Behavior_CoverPoint.h"
|
||||
#include "Components/ArrowComponent.h"
|
||||
#include "Components/BillboardComponent.h"
|
||||
#include "Engine/World.h"
|
||||
#include "CollisionQueryParams.h"
|
||||
|
||||
APS_AI_Behavior_CoverPoint::APS_AI_Behavior_CoverPoint()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = false;
|
||||
bNetLoadOnClient = true;
|
||||
|
||||
USceneComponent* Root = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
|
||||
RootComponent = Root;
|
||||
|
||||
#if WITH_EDITORONLY_DATA
|
||||
ArrowComp = CreateDefaultSubobject<UArrowComponent>(TEXT("Arrow"));
|
||||
ArrowComp->SetupAttachment(Root);
|
||||
ArrowComp->SetArrowSize(0.5f);
|
||||
ArrowComp->SetArrowLength(80.0f);
|
||||
ArrowComp->SetRelativeLocation(FVector(0, 0, 50.0f));
|
||||
ArrowComp->bIsScreenSizeScaled = false;
|
||||
|
||||
SpriteComp = CreateDefaultSubobject<UBillboardComponent>(TEXT("Sprite"));
|
||||
SpriteComp->SetupAttachment(Root);
|
||||
SpriteComp->SetRelativeLocation(FVector(0, 0, 80.0f));
|
||||
SpriteComp->bIsScreenSizeScaled = true;
|
||||
SpriteComp->ScreenSize = 0.0025f;
|
||||
#endif
|
||||
}
|
||||
|
||||
void APS_AI_Behavior_CoverPoint::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
// Cleanup stale occupant refs
|
||||
CurrentOccupants.RemoveAll([](const TWeakObjectPtr<AActor>& Ptr) { return !Ptr.IsValid(); });
|
||||
}
|
||||
|
||||
bool APS_AI_Behavior_CoverPoint::IsAccessibleTo(EPS_AI_Behavior_NPCType NPCType) const
|
||||
{
|
||||
if (!bEnabled) return false;
|
||||
if (AllowedNPCType == EPS_AI_Behavior_NPCType::Any) return true;
|
||||
return AllowedNPCType == NPCType;
|
||||
}
|
||||
|
||||
bool APS_AI_Behavior_CoverPoint::HasRoom() const
|
||||
{
|
||||
if (!bEnabled) return false;
|
||||
|
||||
// Cleanup stale refs
|
||||
int32 ValidCount = 0;
|
||||
for (const TWeakObjectPtr<AActor>& Occ : CurrentOccupants)
|
||||
{
|
||||
if (Occ.IsValid()) ++ValidCount;
|
||||
}
|
||||
return ValidCount < MaxOccupants;
|
||||
}
|
||||
|
||||
bool APS_AI_Behavior_CoverPoint::Claim(AActor* Occupant)
|
||||
{
|
||||
if (!Occupant || !bEnabled) return false;
|
||||
|
||||
// Already claimed by this occupant?
|
||||
for (const TWeakObjectPtr<AActor>& Occ : CurrentOccupants)
|
||||
{
|
||||
if (Occ.Get() == Occupant) return true;
|
||||
}
|
||||
|
||||
// Cleanup stale
|
||||
CurrentOccupants.RemoveAll([](const TWeakObjectPtr<AActor>& Ptr) { return !Ptr.IsValid(); });
|
||||
|
||||
if (CurrentOccupants.Num() >= MaxOccupants)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
CurrentOccupants.Add(Occupant);
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[CoverPoint %s] Claimed by '%s' (%d/%d occupants)"),
|
||||
*GetName(), *Occupant->GetName(), CurrentOccupants.Num(), MaxOccupants);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void APS_AI_Behavior_CoverPoint::Release(AActor* Occupant)
|
||||
{
|
||||
if (!Occupant) return;
|
||||
|
||||
const int32 Removed = CurrentOccupants.RemoveAll([Occupant](const TWeakObjectPtr<AActor>& Ptr)
|
||||
{
|
||||
return !Ptr.IsValid() || Ptr.Get() == Occupant;
|
||||
});
|
||||
|
||||
if (Removed > 0)
|
||||
{
|
||||
UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[CoverPoint %s] Released by '%s' (%d/%d occupants)"),
|
||||
*GetName(), *Occupant->GetName(), CurrentOccupants.Num(), MaxOccupants);
|
||||
}
|
||||
}
|
||||
|
||||
FVector APS_AI_Behavior_CoverPoint::GetCoverDirection() const
|
||||
{
|
||||
return GetActorForwardVector();
|
||||
}
|
||||
|
||||
float APS_AI_Behavior_CoverPoint::EvaluateAgainstThreat(const FVector& ThreatLocation) const
|
||||
{
|
||||
if (!bEnabled) return 0.0f;
|
||||
|
||||
const UWorld* World = GetWorld();
|
||||
if (!World) return Quality;
|
||||
|
||||
const FVector CoverPos = GetActorLocation();
|
||||
float Score = Quality * 0.5f; // Manual quality = half the score
|
||||
|
||||
// Raycast check at crouch and standing heights
|
||||
FCollisionQueryParams Params(SCENE_QUERY_STAT(CoverPointEval), true);
|
||||
Params.AddIgnoredActor(this);
|
||||
|
||||
const float Heights[] = { 60.0f, 100.0f, 170.0f };
|
||||
int32 BlockedCount = 0;
|
||||
|
||||
for (float H : Heights)
|
||||
{
|
||||
FHitResult Hit;
|
||||
const FVector TraceStart = CoverPos + FVector(0, 0, H);
|
||||
const FVector TraceEnd = ThreatLocation + FVector(0, 0, 150.0f);
|
||||
|
||||
if (World->LineTraceSingleByChannel(Hit, TraceStart, TraceEnd, ECC_Visibility, Params))
|
||||
{
|
||||
++BlockedCount;
|
||||
}
|
||||
}
|
||||
|
||||
// Raycast score = other half
|
||||
const float RaycastScore = static_cast<float>(BlockedCount) / UE_ARRAY_COUNT(Heights);
|
||||
Score += RaycastScore * 0.5f;
|
||||
|
||||
return FMath::Clamp(Score, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
void APS_AI_Behavior_CoverPoint::SetEnabled(bool bNewEnabled)
|
||||
{
|
||||
bEnabled = bNewEnabled;
|
||||
|
||||
if (!bEnabled)
|
||||
{
|
||||
// Release all occupants
|
||||
for (const TWeakObjectPtr<AActor>& Occ : CurrentOccupants)
|
||||
{
|
||||
// Occupants will re-evaluate in their next BT tick
|
||||
}
|
||||
CurrentOccupants.Empty();
|
||||
}
|
||||
|
||||
UpdateVisualization();
|
||||
}
|
||||
|
||||
void APS_AI_Behavior_CoverPoint::UpdateVisualization()
|
||||
{
|
||||
#if WITH_EDITORONLY_DATA
|
||||
if (!ArrowComp) return;
|
||||
|
||||
FLinearColor Color = FLinearColor::White;
|
||||
switch (PointType)
|
||||
{
|
||||
case EPS_AI_Behavior_CoverPointType::Cover:
|
||||
Color = FLinearColor(0.2f, 0.5f, 1.0f); // Blue
|
||||
break;
|
||||
case EPS_AI_Behavior_CoverPointType::HidingSpot:
|
||||
Color = FLinearColor(1.0f, 0.85f, 0.0f); // Yellow
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (!bEnabled)
|
||||
{
|
||||
Color *= 0.3f; // Dimmed when disabled
|
||||
}
|
||||
|
||||
ArrowComp->SetArrowColor(Color);
|
||||
#endif
|
||||
}
|
||||
|
||||
#if WITH_EDITOR
|
||||
void APS_AI_Behavior_CoverPoint::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
|
||||
{
|
||||
Super::PostEditChangeProperty(PropertyChangedEvent);
|
||||
UpdateVisualization();
|
||||
}
|
||||
#endif
|
||||
@ -0,0 +1,5 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#include "PS_AI_Behavior_Definitions.h"
|
||||
|
||||
DEFINE_LOG_CATEGORY(LogPS_AI_Behavior);
|
||||
@ -0,0 +1,7 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#include "PS_AI_Behavior_Interface.h"
|
||||
|
||||
// UInterface boilerplate — no default implementation needed.
|
||||
// All functions are BlueprintNativeEvent and must be implemented
|
||||
// by the class that declares "implements IPS_AI_Behavior".
|
||||
@ -0,0 +1,326 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#include "PS_AI_Behavior_PerceptionComponent.h"
|
||||
#include "PS_AI_Behavior_Interface.h"
|
||||
#include "PS_AI_Behavior_PersonalityComponent.h"
|
||||
#include "PS_AI_Behavior_PersonalityProfile.h"
|
||||
#include "PS_AI_Behavior_Settings.h"
|
||||
#include "Perception/AISenseConfig_Sight.h"
|
||||
#include "Perception/AISenseConfig_Hearing.h"
|
||||
#include "Perception/AISenseConfig_Damage.h"
|
||||
#include "Perception/AISense_Sight.h"
|
||||
#include "Perception/AISense_Hearing.h"
|
||||
#include "Perception/AISense_Damage.h"
|
||||
#include "GameFramework/Pawn.h"
|
||||
#include "AIController.h"
|
||||
|
||||
UPS_AI_Behavior_PerceptionComponent::UPS_AI_Behavior_PerceptionComponent()
|
||||
{
|
||||
// Senses are configured in BeginPlay after settings are available
|
||||
}
|
||||
|
||||
void UPS_AI_Behavior_PerceptionComponent::BeginPlay()
|
||||
{
|
||||
ConfigureSenses();
|
||||
Super::BeginPlay();
|
||||
|
||||
OnPerceptionUpdated.AddDynamic(this, &UPS_AI_Behavior_PerceptionComponent::HandlePerceptionUpdated);
|
||||
}
|
||||
|
||||
void UPS_AI_Behavior_PerceptionComponent::ConfigureSenses()
|
||||
{
|
||||
const UPS_AI_Behavior_Settings* Settings = GetDefault<UPS_AI_Behavior_Settings>();
|
||||
|
||||
// ─── Sight ──────────────────────────────────────────────────────────
|
||||
UAISenseConfig_Sight* SightConfig = NewObject<UAISenseConfig_Sight>(this);
|
||||
SightConfig->SightRadius = Settings->DefaultSightRadius;
|
||||
SightConfig->LoseSightRadius = Settings->DefaultSightRadius * 1.2f;
|
||||
SightConfig->PeripheralVisionAngleDegrees = Settings->DefaultSightHalfAngle;
|
||||
SightConfig->SetMaxAge(Settings->PerceptionMaxAge);
|
||||
SightConfig->AutoSuccessRangeFromLastSeenLocation = 500.0f;
|
||||
// Detect ALL affiliations — target filtering is handled by TargetPriority
|
||||
// in GetHighestThreatActor(), not at the perception level.
|
||||
// This is necessary because an Enemy needs to *see* Civilians to target them.
|
||||
SightConfig->DetectionByAffiliation.bDetectEnemies = true;
|
||||
SightConfig->DetectionByAffiliation.bDetectNeutrals = true;
|
||||
SightConfig->DetectionByAffiliation.bDetectFriendlies = true;
|
||||
ConfigureSense(*SightConfig);
|
||||
|
||||
// ─── Hearing ────────────────────────────────────────────────────────
|
||||
UAISenseConfig_Hearing* HearingConfig = NewObject<UAISenseConfig_Hearing>(this);
|
||||
HearingConfig->HearingRange = Settings->DefaultHearingRange;
|
||||
HearingConfig->SetMaxAge(Settings->PerceptionMaxAge);
|
||||
HearingConfig->DetectionByAffiliation.bDetectEnemies = true;
|
||||
HearingConfig->DetectionByAffiliation.bDetectNeutrals = true;
|
||||
HearingConfig->DetectionByAffiliation.bDetectFriendlies = true;
|
||||
ConfigureSense(*HearingConfig);
|
||||
|
||||
// ─── Damage ─────────────────────────────────────────────────────────
|
||||
UAISenseConfig_Damage* DamageConfig = NewObject<UAISenseConfig_Damage>(this);
|
||||
DamageConfig->SetMaxAge(Settings->PerceptionMaxAge * 2.0f); // Damage memories last longer
|
||||
ConfigureSense(*DamageConfig);
|
||||
|
||||
// Sight is the dominant sense
|
||||
SetDominantSense(UAISense_Sight::StaticClass());
|
||||
}
|
||||
|
||||
void UPS_AI_Behavior_PerceptionComponent::HandlePerceptionUpdated(const TArray<AActor*>& UpdatedActors)
|
||||
{
|
||||
// Placeholder — BTService_UpdateThreat does the heavy lifting.
|
||||
// This callback can be used for immediate alert reactions.
|
||||
}
|
||||
|
||||
// ─── Actor Classification ───────────────────────────────────────────────────
|
||||
|
||||
EPS_AI_Behavior_TargetType UPS_AI_Behavior_PerceptionComponent::ClassifyActor(const AActor* Actor)
|
||||
{
|
||||
if (!Actor) return EPS_AI_Behavior_TargetType::Civilian; // Safe default
|
||||
|
||||
// Check if player-controlled
|
||||
const APawn* Pawn = Cast<APawn>(Actor);
|
||||
if (Pawn && Pawn->IsPlayerControlled())
|
||||
{
|
||||
return EPS_AI_Behavior_TargetType::Player;
|
||||
}
|
||||
|
||||
// Check via IPS_AI_Behavior interface
|
||||
if (Actor->Implements<UPS_AI_Behavior_Interface>())
|
||||
{
|
||||
const EPS_AI_Behavior_NPCType NPCType =
|
||||
IPS_AI_Behavior_Interface::Execute_GetBehaviorNPCType(const_cast<AActor*>(Actor));
|
||||
|
||||
switch (NPCType)
|
||||
{
|
||||
case EPS_AI_Behavior_NPCType::Civilian: return EPS_AI_Behavior_TargetType::Civilian;
|
||||
case EPS_AI_Behavior_NPCType::Enemy: return EPS_AI_Behavior_TargetType::Enemy;
|
||||
case EPS_AI_Behavior_NPCType::Protector: return EPS_AI_Behavior_TargetType::Protector;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: check PersonalityComponent
|
||||
if (Pawn)
|
||||
{
|
||||
if (const auto* PersonalityComp = Pawn->FindComponentByClass<UPS_AI_Behavior_PersonalityComponent>())
|
||||
{
|
||||
switch (PersonalityComp->GetNPCType())
|
||||
{
|
||||
case EPS_AI_Behavior_NPCType::Civilian: return EPS_AI_Behavior_TargetType::Civilian;
|
||||
case EPS_AI_Behavior_NPCType::Enemy: return EPS_AI_Behavior_TargetType::Enemy;
|
||||
case EPS_AI_Behavior_NPCType::Protector: return EPS_AI_Behavior_TargetType::Protector;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return EPS_AI_Behavior_TargetType::Civilian;
|
||||
}
|
||||
|
||||
// ─── Target Selection ───────────────────────────────────────────────────────
|
||||
|
||||
AActor* UPS_AI_Behavior_PerceptionComponent::GetHighestThreatActor()
|
||||
{
|
||||
// Get priority from PersonalityProfile if available
|
||||
TArray<EPS_AI_Behavior_TargetType> Priority;
|
||||
|
||||
const AActor* Owner = GetOwner();
|
||||
if (Owner)
|
||||
{
|
||||
// Owner is the AIController, get the Pawn
|
||||
const AAIController* AIC = Cast<AAIController>(Owner);
|
||||
const APawn* MyPawn = AIC ? AIC->GetPawn() : Cast<APawn>(const_cast<AActor*>(Owner));
|
||||
|
||||
if (MyPawn)
|
||||
{
|
||||
if (const auto* PersonalityComp = MyPawn->FindComponentByClass<UPS_AI_Behavior_PersonalityComponent>())
|
||||
{
|
||||
if (PersonalityComp->Profile)
|
||||
{
|
||||
Priority = PersonalityComp->Profile->TargetPriority;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return GetHighestThreatActor(Priority);
|
||||
}
|
||||
|
||||
AActor* UPS_AI_Behavior_PerceptionComponent::GetHighestThreatActor(
|
||||
const TArray<EPS_AI_Behavior_TargetType>& TargetPriority)
|
||||
{
|
||||
// Gather all perceived actors from all senses
|
||||
TArray<AActor*> PerceivedActors;
|
||||
GetCurrentlyPerceivedActors(nullptr, PerceivedActors); // All senses at once
|
||||
|
||||
if (PerceivedActors.Num() == 0)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const AActor* Owner = GetOwner();
|
||||
if (!Owner)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Use default priority if none provided
|
||||
static const TArray<EPS_AI_Behavior_TargetType> DefaultPriority = {
|
||||
EPS_AI_Behavior_TargetType::Protector,
|
||||
EPS_AI_Behavior_TargetType::Player,
|
||||
EPS_AI_Behavior_TargetType::Civilian,
|
||||
};
|
||||
const TArray<EPS_AI_Behavior_TargetType>& ActivePriority =
|
||||
TargetPriority.Num() > 0 ? TargetPriority : DefaultPriority;
|
||||
|
||||
const FVector OwnerLoc = Owner->GetActorLocation();
|
||||
AActor* BestThreat = nullptr;
|
||||
float BestScore = -1.0f;
|
||||
|
||||
for (AActor* Actor : PerceivedActors)
|
||||
{
|
||||
if (!Actor || Actor == Owner) continue;
|
||||
|
||||
// Skip self (when owner is AIController, also skip own pawn)
|
||||
const AAIController* AIC = Cast<AAIController>(Owner);
|
||||
if (AIC && Actor == AIC->GetPawn()) continue;
|
||||
|
||||
// ─── Classify this actor ────────────────────────────────────────
|
||||
const EPS_AI_Behavior_TargetType ActorType = ClassifyActor(Actor);
|
||||
|
||||
// Check if this target type is in our priority list at all
|
||||
const int32 PriorityIndex = ActivePriority.IndexOfByKey(ActorType);
|
||||
if (PriorityIndex == INDEX_NONE)
|
||||
{
|
||||
// Not a valid target for this NPC
|
||||
continue;
|
||||
}
|
||||
|
||||
// ─── Score calculation ──────────────────────────────────────────
|
||||
float Score = 0.0f;
|
||||
|
||||
// Priority rank bonus: higher priority = much higher score
|
||||
// Max priority entries = ~4, so (4 - index) * 100 gives clear separation
|
||||
Score += (ActivePriority.Num() - PriorityIndex) * 100.0f;
|
||||
|
||||
// Damage sense override: actor that hit us gets a massive bonus
|
||||
// (bypasses priority — self-defense)
|
||||
FActorPerceptionBlueprintInfo Info;
|
||||
if (GetActorsPerception(Actor, Info))
|
||||
{
|
||||
for (const FAIStimulus& Stimulus : Info.LastSensedStimuli)
|
||||
{
|
||||
if (!Stimulus.IsValid()) continue;
|
||||
|
||||
if (Stimulus.Type == UAISense::GetSenseID<UAISense_Damage>())
|
||||
{
|
||||
Score += 500.0f; // Self-defense: always prioritize attacker
|
||||
}
|
||||
else if (Stimulus.Type == UAISense::GetSenseID<UAISense_Sight>())
|
||||
{
|
||||
Score += 10.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
Score += 5.0f; // Hearing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Distance: closer targets score higher (0-20 range)
|
||||
const float Dist = FVector::Dist(OwnerLoc, Actor->GetActorLocation());
|
||||
Score += FMath::GetMappedRangeValueClamped(
|
||||
FVector2D(0.0f, 6000.0f), FVector2D(20.0f, 0.0f), Dist);
|
||||
|
||||
if (Score > BestScore)
|
||||
{
|
||||
BestScore = Score;
|
||||
BestThreat = Actor;
|
||||
}
|
||||
}
|
||||
|
||||
return BestThreat;
|
||||
}
|
||||
|
||||
float UPS_AI_Behavior_PerceptionComponent::CalculateThreatLevel()
|
||||
{
|
||||
const AActor* Owner = GetOwner();
|
||||
if (!Owner) return 0.0f;
|
||||
|
||||
const FVector OwnerLoc = Owner->GetActorLocation();
|
||||
float TotalThreat = 0.0f;
|
||||
|
||||
TArray<AActor*> PerceivedActors;
|
||||
GetCurrentlyPerceivedActors(nullptr, PerceivedActors); // All senses
|
||||
|
||||
for (AActor* Actor : PerceivedActors)
|
||||
{
|
||||
if (!Actor) continue;
|
||||
|
||||
float ActorThreat = 0.0f;
|
||||
const float Dist = FVector::Dist(OwnerLoc, Actor->GetActorLocation());
|
||||
|
||||
// Closer = more threatening
|
||||
ActorThreat += FMath::GetMappedRangeValueClamped(
|
||||
FVector2D(0.0f, 5000.0f), FVector2D(0.5f, 0.05f), Dist);
|
||||
|
||||
// Sense-based multiplier
|
||||
FActorPerceptionBlueprintInfo Info;
|
||||
if (GetActorsPerception(Actor, Info))
|
||||
{
|
||||
for (const FAIStimulus& Stimulus : Info.LastSensedStimuli)
|
||||
{
|
||||
if (!Stimulus.IsValid()) continue;
|
||||
|
||||
if (Stimulus.Type == UAISense::GetSenseID<UAISense_Damage>())
|
||||
{
|
||||
ActorThreat += 0.6f; // Being hit = big threat spike
|
||||
}
|
||||
else if (Stimulus.Type == UAISense::GetSenseID<UAISense_Sight>())
|
||||
{
|
||||
ActorThreat += 0.2f;
|
||||
}
|
||||
else if (Stimulus.Type == UAISense::GetSenseID<UAISense_Hearing>())
|
||||
{
|
||||
ActorThreat += 0.1f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TotalThreat += ActorThreat;
|
||||
}
|
||||
|
||||
// Clamp to reasonable range (can exceed 1.0 for multiple threats)
|
||||
return FMath::Min(TotalThreat, 2.0f);
|
||||
}
|
||||
|
||||
bool UPS_AI_Behavior_PerceptionComponent::GetThreatLocation(FVector& OutLocation)
|
||||
{
|
||||
AActor* Threat = GetHighestThreatActor();
|
||||
if (Threat)
|
||||
{
|
||||
OutLocation = Threat->GetActorLocation();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fallback: check last known stimulus location
|
||||
TArray<AActor*> KnownActors;
|
||||
GetKnownPerceivedActors(nullptr, KnownActors);
|
||||
|
||||
if (KnownActors.Num() > 0 && KnownActors[0])
|
||||
{
|
||||
FActorPerceptionBlueprintInfo Info;
|
||||
if (GetActorsPerception(KnownActors[0], Info))
|
||||
{
|
||||
for (const FAIStimulus& Stimulus : Info.LastSensedStimuli)
|
||||
{
|
||||
if (Stimulus.IsValid())
|
||||
{
|
||||
OutLocation = Stimulus.StimulusLocation;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@ -0,0 +1,189 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#include "PS_AI_Behavior_PersonalityComponent.h"
|
||||
#include "PS_AI_Behavior_Interface.h"
|
||||
#include "PS_AI_Behavior_PersonalityProfile.h"
|
||||
#include "Net/UnrealNetwork.h"
|
||||
|
||||
UPS_AI_Behavior_PersonalityComponent::UPS_AI_Behavior_PersonalityComponent()
|
||||
{
|
||||
PrimaryComponentTick.bCanEverTick = false;
|
||||
SetIsReplicatedByDefault(true);
|
||||
}
|
||||
|
||||
void UPS_AI_Behavior_PersonalityComponent::GetLifetimeReplicatedProps(
|
||||
TArray<FLifetimeProperty>& OutLifetimeProps) const
|
||||
{
|
||||
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
|
||||
|
||||
DOREPLIFETIME(UPS_AI_Behavior_PersonalityComponent, CurrentState);
|
||||
DOREPLIFETIME(UPS_AI_Behavior_PersonalityComponent, PerceivedThreatLevel);
|
||||
}
|
||||
|
||||
void UPS_AI_Behavior_PersonalityComponent::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
// Initialize runtime traits from profile
|
||||
if (Profile)
|
||||
{
|
||||
RuntimeTraits = Profile->TraitScores;
|
||||
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] Personality initialized from profile '%s' (%s)"),
|
||||
*GetOwner()->GetName(),
|
||||
*Profile->ProfileName.ToString(),
|
||||
*UEnum::GetValueAsString(Profile->NPCType));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Defaults — all traits at 0.5
|
||||
for (uint8 i = 0; i <= static_cast<uint8>(EPS_AI_Behavior_TraitAxis::Discipline); ++i)
|
||||
{
|
||||
RuntimeTraits.Add(static_cast<EPS_AI_Behavior_TraitAxis>(i), 0.5f);
|
||||
}
|
||||
UE_LOG(LogPS_AI_Behavior, Warning, TEXT("[%s] No PersonalityProfile assigned — using default traits."),
|
||||
*GetOwner()->GetName());
|
||||
}
|
||||
}
|
||||
|
||||
float UPS_AI_Behavior_PersonalityComponent::GetTrait(EPS_AI_Behavior_TraitAxis Axis) const
|
||||
{
|
||||
const float* Found = RuntimeTraits.Find(Axis);
|
||||
return Found ? *Found : 0.5f;
|
||||
}
|
||||
|
||||
void UPS_AI_Behavior_PersonalityComponent::ModifyTrait(EPS_AI_Behavior_TraitAxis Axis, float Delta)
|
||||
{
|
||||
float& Value = RuntimeTraits.FindOrAdd(Axis, 0.5f);
|
||||
Value = FMath::Clamp(Value + Delta, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
EPS_AI_Behavior_State UPS_AI_Behavior_PersonalityComponent::EvaluateReaction() const
|
||||
{
|
||||
if (CurrentState == EPS_AI_Behavior_State::Dead)
|
||||
{
|
||||
return EPS_AI_Behavior_State::Dead;
|
||||
}
|
||||
|
||||
const float Courage = GetTrait(EPS_AI_Behavior_TraitAxis::Courage);
|
||||
const float Aggressivity = GetTrait(EPS_AI_Behavior_TraitAxis::Aggressivity);
|
||||
const float Caution = GetTrait(EPS_AI_Behavior_TraitAxis::Caution);
|
||||
|
||||
// Get thresholds from profile (or use defaults)
|
||||
float FleeThresh = Profile ? Profile->FleeThreshold : 0.5f;
|
||||
float AttackThresh = Profile ? Profile->AttackThreshold : 0.4f;
|
||||
float AlertThresh = Profile ? Profile->AlertThreshold : 0.15f;
|
||||
|
||||
// Modulate thresholds by personality:
|
||||
// - High courage raises the flee threshold (harder to scare)
|
||||
// - High aggressivity lowers the attack threshold (quicker to fight)
|
||||
// - High caution lowers the flee threshold (quicker to run)
|
||||
const float EffectiveFleeThresh = FleeThresh * (0.5f + Courage * 0.5f) * (1.5f - Caution * 0.5f);
|
||||
const float EffectiveAttackThresh = AttackThresh * (1.5f - Aggressivity * 0.5f);
|
||||
|
||||
// Decision cascade
|
||||
if (PerceivedThreatLevel >= EffectiveFleeThresh && Courage < 0.7f)
|
||||
{
|
||||
return EPS_AI_Behavior_State::Fleeing;
|
||||
}
|
||||
|
||||
if (PerceivedThreatLevel >= EffectiveAttackThresh && Aggressivity > 0.3f)
|
||||
{
|
||||
// Cautious NPCs prefer cover over direct combat
|
||||
if (Caution > 0.6f)
|
||||
{
|
||||
return EPS_AI_Behavior_State::TakingCover;
|
||||
}
|
||||
return EPS_AI_Behavior_State::Combat;
|
||||
}
|
||||
|
||||
if (PerceivedThreatLevel >= AlertThresh)
|
||||
{
|
||||
return EPS_AI_Behavior_State::Alerted;
|
||||
}
|
||||
|
||||
// No threat — maintain patrol or idle
|
||||
return (CurrentState == EPS_AI_Behavior_State::Patrol)
|
||||
? EPS_AI_Behavior_State::Patrol
|
||||
: EPS_AI_Behavior_State::Idle;
|
||||
}
|
||||
|
||||
EPS_AI_Behavior_State UPS_AI_Behavior_PersonalityComponent::ApplyReaction()
|
||||
{
|
||||
const EPS_AI_Behavior_State NewState = EvaluateReaction();
|
||||
if (NewState != CurrentState)
|
||||
{
|
||||
const EPS_AI_Behavior_State OldState = CurrentState;
|
||||
CurrentState = NewState; // Replicated → OnRep fires on clients
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[%s] State: %s -> %s (Threat: %.2f)"),
|
||||
*GetOwner()->GetName(),
|
||||
*UEnum::GetValueAsString(OldState),
|
||||
*UEnum::GetValueAsString(NewState),
|
||||
PerceivedThreatLevel);
|
||||
|
||||
HandleStateChanged(OldState, NewState);
|
||||
}
|
||||
return CurrentState;
|
||||
}
|
||||
|
||||
void UPS_AI_Behavior_PersonalityComponent::ForceState(EPS_AI_Behavior_State NewState)
|
||||
{
|
||||
if (NewState != CurrentState)
|
||||
{
|
||||
const EPS_AI_Behavior_State OldState = CurrentState;
|
||||
CurrentState = NewState; // Replicated → OnRep fires on clients
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] State forced: %s -> %s"),
|
||||
*GetOwner()->GetName(),
|
||||
*UEnum::GetValueAsString(OldState),
|
||||
*UEnum::GetValueAsString(NewState));
|
||||
|
||||
HandleStateChanged(OldState, NewState);
|
||||
}
|
||||
}
|
||||
|
||||
void UPS_AI_Behavior_PersonalityComponent::OnRep_CurrentState(EPS_AI_Behavior_State OldState)
|
||||
{
|
||||
// On clients: fire delegate only (speed is set by CMC replication)
|
||||
OnBehaviorStateChanged.Broadcast(OldState, CurrentState);
|
||||
|
||||
// Also notify the Pawn via interface (for client-side cosmetics)
|
||||
AActor* Owner = GetOwner();
|
||||
if (Owner && Owner->Implements<UPS_AI_Behavior_Interface>())
|
||||
{
|
||||
IPS_AI_Behavior_Interface::Execute_OnBehaviorStateChanged(Owner, CurrentState, OldState);
|
||||
}
|
||||
}
|
||||
|
||||
void UPS_AI_Behavior_PersonalityComponent::HandleStateChanged(
|
||||
EPS_AI_Behavior_State OldState, EPS_AI_Behavior_State NewState)
|
||||
{
|
||||
// 1. Broadcast delegate (server)
|
||||
OnBehaviorStateChanged.Broadcast(OldState, NewState);
|
||||
|
||||
AActor* Owner = GetOwner();
|
||||
if (!Owner) return;
|
||||
|
||||
// 2. Set movement speed via interface
|
||||
if (Owner->Implements<UPS_AI_Behavior_Interface>())
|
||||
{
|
||||
float NewSpeed = Profile ? Profile->GetSpeedForState(NewState) : 150.0f;
|
||||
IPS_AI_Behavior_Interface::Execute_SetBehaviorMovementSpeed(Owner, NewSpeed);
|
||||
|
||||
// 3. Notify the Pawn of the state change
|
||||
IPS_AI_Behavior_Interface::Execute_OnBehaviorStateChanged(Owner, NewState, OldState);
|
||||
}
|
||||
}
|
||||
|
||||
EPS_AI_Behavior_NPCType UPS_AI_Behavior_PersonalityComponent::GetNPCType() const
|
||||
{
|
||||
// Prefer the IPS_AI_Behavior interface on the owning actor
|
||||
AActor* Owner = GetOwner();
|
||||
if (Owner && Owner->Implements<UPS_AI_Behavior_Interface>())
|
||||
{
|
||||
return IPS_AI_Behavior_Interface::Execute_GetBehaviorNPCType(Owner);
|
||||
}
|
||||
|
||||
// Fallback: read from PersonalityProfile
|
||||
return Profile ? Profile->NPCType : EPS_AI_Behavior_NPCType::Civilian;
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#include "PS_AI_Behavior_PersonalityProfile.h"
|
||||
|
||||
UPS_AI_Behavior_PersonalityProfile::UPS_AI_Behavior_PersonalityProfile()
|
||||
{
|
||||
// Initialize all trait axes to a neutral 0.5
|
||||
TraitScores.Add(EPS_AI_Behavior_TraitAxis::Courage, 0.5f);
|
||||
TraitScores.Add(EPS_AI_Behavior_TraitAxis::Aggressivity, 0.5f);
|
||||
TraitScores.Add(EPS_AI_Behavior_TraitAxis::Loyalty, 0.5f);
|
||||
TraitScores.Add(EPS_AI_Behavior_TraitAxis::Caution, 0.5f);
|
||||
TraitScores.Add(EPS_AI_Behavior_TraitAxis::Discipline, 0.5f);
|
||||
|
||||
// Default target priority: Protector first, then Player, then Civilian
|
||||
TargetPriority.Add(EPS_AI_Behavior_TargetType::Protector);
|
||||
TargetPriority.Add(EPS_AI_Behavior_TargetType::Player);
|
||||
TargetPriority.Add(EPS_AI_Behavior_TargetType::Civilian);
|
||||
|
||||
// Default speeds per state (cm/s)
|
||||
SpeedPerState.Add(EPS_AI_Behavior_State::Idle, 0.0f);
|
||||
SpeedPerState.Add(EPS_AI_Behavior_State::Patrol, 150.0f);
|
||||
SpeedPerState.Add(EPS_AI_Behavior_State::Alerted, 200.0f);
|
||||
SpeedPerState.Add(EPS_AI_Behavior_State::Combat, 350.0f);
|
||||
SpeedPerState.Add(EPS_AI_Behavior_State::Fleeing, 500.0f);
|
||||
SpeedPerState.Add(EPS_AI_Behavior_State::TakingCover, 400.0f);
|
||||
SpeedPerState.Add(EPS_AI_Behavior_State::Dead, 0.0f);
|
||||
}
|
||||
|
||||
float UPS_AI_Behavior_PersonalityProfile::GetTrait(EPS_AI_Behavior_TraitAxis Axis) const
|
||||
{
|
||||
const float* Found = TraitScores.Find(Axis);
|
||||
return Found ? *Found : 0.5f;
|
||||
}
|
||||
|
||||
float UPS_AI_Behavior_PersonalityProfile::GetSpeedForState(EPS_AI_Behavior_State State) const
|
||||
{
|
||||
const float* Found = SpeedPerState.Find(State);
|
||||
return Found ? *Found : DefaultWalkSpeed;
|
||||
}
|
||||
|
||||
FPrimaryAssetId UPS_AI_Behavior_PersonalityProfile::GetPrimaryAssetId() const
|
||||
{
|
||||
return FPrimaryAssetId(TEXT("PersonalityProfile"), GetFName());
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#include "PS_AI_Behavior_Settings.h"
|
||||
|
||||
UPS_AI_Behavior_Settings::UPS_AI_Behavior_Settings()
|
||||
{
|
||||
}
|
||||
@ -0,0 +1,311 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#include "PS_AI_Behavior_SplineFollowerComponent.h"
|
||||
#include "PS_AI_Behavior_SplinePath.h"
|
||||
#include "PS_AI_Behavior_SplineNetwork.h"
|
||||
#include "PS_AI_Behavior_PersonalityComponent.h"
|
||||
#include "Components/SplineComponent.h"
|
||||
#include "GameFramework/Character.h"
|
||||
#include "GameFramework/CharacterMovementComponent.h"
|
||||
#include "Net/UnrealNetwork.h"
|
||||
|
||||
UPS_AI_Behavior_SplineFollowerComponent::UPS_AI_Behavior_SplineFollowerComponent()
|
||||
{
|
||||
PrimaryComponentTick.bCanEverTick = true;
|
||||
PrimaryComponentTick.TickGroup = TG_PrePhysics;
|
||||
SetIsReplicatedByDefault(true);
|
||||
|
||||
// Each NPC walks at a slightly different speed for natural look
|
||||
SpeedVariation = FMath::RandRange(0.85f, 1.15f);
|
||||
}
|
||||
|
||||
void UPS_AI_Behavior_SplineFollowerComponent::GetLifetimeReplicatedProps(
|
||||
TArray<FLifetimeProperty>& OutLifetimeProps) const
|
||||
{
|
||||
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
|
||||
|
||||
DOREPLIFETIME(UPS_AI_Behavior_SplineFollowerComponent, CurrentSpline);
|
||||
DOREPLIFETIME(UPS_AI_Behavior_SplineFollowerComponent, bIsFollowing);
|
||||
}
|
||||
|
||||
bool UPS_AI_Behavior_SplineFollowerComponent::StartFollowing(
|
||||
APS_AI_Behavior_SplinePath* Spline, bool bForward)
|
||||
{
|
||||
if (!Spline || !GetOwner())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Snap to closest point on spline
|
||||
float Dist = 0.0f;
|
||||
FVector ClosestPoint;
|
||||
Spline->GetClosestPointOnSpline(GetOwner()->GetActorLocation(), Dist, ClosestPoint);
|
||||
|
||||
return StartFollowingAtDistance(Spline, Dist, bForward);
|
||||
}
|
||||
|
||||
bool UPS_AI_Behavior_SplineFollowerComponent::StartFollowingAtDistance(
|
||||
APS_AI_Behavior_SplinePath* Spline, float StartDistance, bool bForward)
|
||||
{
|
||||
if (!Spline)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
APS_AI_Behavior_SplinePath* OldSpline = CurrentSpline;
|
||||
CurrentSpline = Spline;
|
||||
CurrentDistance = FMath::Clamp(StartDistance, 0.0f, Spline->GetSplineLength());
|
||||
bMovingForward = bForward;
|
||||
bIsFollowing = true;
|
||||
LastHandledJunctionIndex = -1;
|
||||
|
||||
if (OldSpline && OldSpline != Spline)
|
||||
{
|
||||
OnSplineChanged.Broadcast(OldSpline, Spline);
|
||||
}
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[%s] Started following spline '%s' at d=%.0f (%s)"),
|
||||
*GetOwner()->GetName(), *Spline->GetName(), CurrentDistance,
|
||||
bForward ? TEXT("forward") : TEXT("reverse"));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void UPS_AI_Behavior_SplineFollowerComponent::StopFollowing()
|
||||
{
|
||||
bIsFollowing = false;
|
||||
UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[%s] Stopped following spline."),
|
||||
GetOwner() ? *GetOwner()->GetName() : TEXT("?"));
|
||||
}
|
||||
|
||||
void UPS_AI_Behavior_SplineFollowerComponent::PauseFollowing()
|
||||
{
|
||||
bIsFollowing = false;
|
||||
}
|
||||
|
||||
void UPS_AI_Behavior_SplineFollowerComponent::ResumeFollowing()
|
||||
{
|
||||
if (CurrentSpline)
|
||||
{
|
||||
bIsFollowing = true;
|
||||
}
|
||||
}
|
||||
|
||||
void UPS_AI_Behavior_SplineFollowerComponent::SwitchToSpline(
|
||||
APS_AI_Behavior_SplinePath* NewSpline, float DistanceOnNew, bool bNewForward)
|
||||
{
|
||||
if (!NewSpline) return;
|
||||
|
||||
APS_AI_Behavior_SplinePath* OldSpline = CurrentSpline;
|
||||
CurrentSpline = NewSpline;
|
||||
CurrentDistance = FMath::Clamp(DistanceOnNew, 0.0f, NewSpline->GetSplineLength());
|
||||
bMovingForward = bNewForward;
|
||||
LastHandledJunctionIndex = -1;
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[%s] Switched to spline '%s' at d=%.0f"),
|
||||
GetOwner() ? *GetOwner()->GetName() : TEXT("?"), *NewSpline->GetName(), CurrentDistance);
|
||||
|
||||
if (OldSpline != NewSpline)
|
||||
{
|
||||
OnSplineChanged.Broadcast(OldSpline, NewSpline);
|
||||
}
|
||||
}
|
||||
|
||||
float UPS_AI_Behavior_SplineFollowerComponent::GetEffectiveSpeed() const
|
||||
{
|
||||
float Speed = DefaultWalkSpeed;
|
||||
|
||||
if (CurrentSpline && CurrentSpline->SplineWalkSpeed > 0.0f)
|
||||
{
|
||||
Speed = CurrentSpline->SplineWalkSpeed;
|
||||
}
|
||||
|
||||
return Speed * SpeedVariation;
|
||||
}
|
||||
|
||||
float UPS_AI_Behavior_SplineFollowerComponent::GetProgress() const
|
||||
{
|
||||
if (!CurrentSpline) return 0.0f;
|
||||
const float Len = CurrentSpline->GetSplineLength();
|
||||
return (Len > 0.0f) ? (CurrentDistance / Len) : 0.0f;
|
||||
}
|
||||
|
||||
void UPS_AI_Behavior_SplineFollowerComponent::TickComponent(
|
||||
float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
|
||||
{
|
||||
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
|
||||
|
||||
if (!bIsFollowing || !CurrentSpline || !GetOwner())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const float SplineLen = CurrentSpline->GetSplineLength();
|
||||
if (SplineLen <= 0.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── Advance along spline ───────────────────────────────────────────
|
||||
const float Speed = GetEffectiveSpeed();
|
||||
const float Delta = Speed * DeltaTime;
|
||||
|
||||
if (bMovingForward)
|
||||
{
|
||||
CurrentDistance += Delta;
|
||||
}
|
||||
else
|
||||
{
|
||||
CurrentDistance -= Delta;
|
||||
}
|
||||
|
||||
// ─── End of spline handling ─────────────────────────────────────────
|
||||
if (CurrentDistance >= SplineLen)
|
||||
{
|
||||
if (CurrentSpline->SplineComp && CurrentSpline->SplineComp->IsClosedLoop())
|
||||
{
|
||||
CurrentDistance = FMath::Fmod(CurrentDistance, SplineLen);
|
||||
}
|
||||
else if (bReverseAtEnd)
|
||||
{
|
||||
CurrentDistance = SplineLen - (CurrentDistance - SplineLen);
|
||||
bMovingForward = false;
|
||||
}
|
||||
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)
|
||||
{
|
||||
CurrentDistance = -CurrentDistance;
|
||||
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();
|
||||
const FVector CurrentLocation = Owner->GetActorLocation();
|
||||
const FRotator CurrentRotation = Owner->GetActorRotation();
|
||||
|
||||
// Use Character movement if available for proper physics/collision
|
||||
ACharacter* Character = Cast<ACharacter>(Owner);
|
||||
if (Character && Character->GetCharacterMovement())
|
||||
{
|
||||
// Compute velocity to reach the spline point
|
||||
FVector DesiredVelocity = (TargetLocation - CurrentLocation) / FMath::Max(DeltaTime, 0.001f);
|
||||
|
||||
// Clamp to avoid teleporting on large frame spikes
|
||||
const float MaxVel = Speed * 3.0f;
|
||||
if (DesiredVelocity.SizeSquared() > MaxVel * MaxVel)
|
||||
{
|
||||
DesiredVelocity = DesiredVelocity.GetSafeNormal() * MaxVel;
|
||||
}
|
||||
|
||||
Character->GetCharacterMovement()->RequestDirectMove(DesiredVelocity, /*bForceMaxSpeed=*/false);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Direct placement for non-Characters
|
||||
Owner->SetActorLocation(TargetLocation);
|
||||
}
|
||||
|
||||
// Smooth rotation — flip if going backward
|
||||
FRotator FinalTargetRot = TargetRotation;
|
||||
if (!bMovingForward)
|
||||
{
|
||||
FinalTargetRot.Yaw += 180.0f;
|
||||
}
|
||||
|
||||
const FRotator SmoothedRot = FMath::RInterpConstantTo(
|
||||
CurrentRotation, FinalTargetRot, DeltaTime, RotationInterpSpeed);
|
||||
Owner->SetActorRotation(FRotator(0.0f, SmoothedRot.Yaw, 0.0f)); // Only yaw
|
||||
|
||||
// ─── Junction handling ──────────────────────────────────────────────
|
||||
HandleJunctions();
|
||||
}
|
||||
|
||||
void UPS_AI_Behavior_SplineFollowerComponent::HandleJunctions()
|
||||
{
|
||||
if (!CurrentSpline) return;
|
||||
|
||||
const TArray<FPS_AI_Behavior_SplineJunction>& Junctions = CurrentSpline->Junctions;
|
||||
if (Junctions.Num() == 0) return;
|
||||
|
||||
for (int32 i = 0; i < Junctions.Num(); ++i)
|
||||
{
|
||||
if (i == LastHandledJunctionIndex)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const FPS_AI_Behavior_SplineJunction& J = Junctions[i];
|
||||
const float DistToJunction = FMath::Abs(J.DistanceOnThisSpline - CurrentDistance);
|
||||
|
||||
// Are we approaching this junction?
|
||||
if (DistToJunction <= JunctionDetectionDistance)
|
||||
{
|
||||
// Check direction: only handle junctions ahead of us
|
||||
if (bMovingForward && J.DistanceOnThisSpline < CurrentDistance)
|
||||
{
|
||||
continue; // Junction is behind us
|
||||
}
|
||||
if (!bMovingForward && J.DistanceOnThisSpline > CurrentDistance)
|
||||
{
|
||||
continue; // Junction is behind us
|
||||
}
|
||||
|
||||
LastHandledJunctionIndex = i;
|
||||
OnApproachingJunction.Broadcast(CurrentSpline, i, DistToJunction);
|
||||
|
||||
if (bAutoChooseAtJunction)
|
||||
{
|
||||
// Use SplineNetwork subsystem to choose
|
||||
UPS_AI_Behavior_SplineNetwork* Network =
|
||||
GetWorld()->GetSubsystem<UPS_AI_Behavior_SplineNetwork>();
|
||||
if (!Network) break;
|
||||
|
||||
// Get NPC type and caution from personality
|
||||
EPS_AI_Behavior_NPCType NPCType = EPS_AI_Behavior_NPCType::Civilian;
|
||||
float Caution = 0.5f;
|
||||
|
||||
UPS_AI_Behavior_PersonalityComponent* Personality =
|
||||
GetOwner()->FindComponentByClass<UPS_AI_Behavior_PersonalityComponent>();
|
||||
if (Personality)
|
||||
{
|
||||
NPCType = Personality->GetNPCType();
|
||||
Caution = Personality->GetTrait(EPS_AI_Behavior_TraitAxis::Caution);
|
||||
}
|
||||
|
||||
APS_AI_Behavior_SplinePath* ChosenSpline = Network->ChooseSplineAtJunction(
|
||||
CurrentSpline, i, NPCType, FVector::ZeroVector, Caution);
|
||||
|
||||
if (ChosenSpline && ChosenSpline != CurrentSpline)
|
||||
{
|
||||
SwitchToSpline(ChosenSpline, J.DistanceOnOtherSpline, bMovingForward);
|
||||
}
|
||||
}
|
||||
|
||||
break; // Only handle one junction per tick
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,257 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#include "PS_AI_Behavior_SplineNetwork.h"
|
||||
#include "PS_AI_Behavior_SplinePath.h"
|
||||
#include "Components/SplineComponent.h"
|
||||
#include "EngineUtils.h"
|
||||
#include "Engine/World.h"
|
||||
|
||||
// ─── Subsystem Lifecycle ────────────────────────────────────────────────────
|
||||
|
||||
void UPS_AI_Behavior_SplineNetwork::Initialize(FSubsystemCollectionBase& Collection)
|
||||
{
|
||||
Super::Initialize(Collection);
|
||||
}
|
||||
|
||||
void UPS_AI_Behavior_SplineNetwork::Deinitialize()
|
||||
{
|
||||
AllSplines.Empty();
|
||||
TotalJunctions = 0;
|
||||
Super::Deinitialize();
|
||||
}
|
||||
|
||||
void UPS_AI_Behavior_SplineNetwork::OnWorldBeginPlay(UWorld& InWorld)
|
||||
{
|
||||
Super::OnWorldBeginPlay(InWorld);
|
||||
RebuildNetwork();
|
||||
}
|
||||
|
||||
// ─── Network Build ──────────────────────────────────────────────────────────
|
||||
|
||||
void UPS_AI_Behavior_SplineNetwork::RebuildNetwork()
|
||||
{
|
||||
UWorld* World = GetWorld();
|
||||
if (!World) return;
|
||||
|
||||
AllSplines.Empty();
|
||||
TotalJunctions = 0;
|
||||
|
||||
// Gather all spline paths
|
||||
for (TActorIterator<APS_AI_Behavior_SplinePath> It(World); It; ++It)
|
||||
{
|
||||
AllSplines.Add(*It);
|
||||
It->Junctions.Empty(); // Reset
|
||||
}
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Log, TEXT("SplineNetwork: Found %d spline paths. Computing junctions..."),
|
||||
AllSplines.Num());
|
||||
|
||||
// Junction detection tolerance (cm) — how close two splines must be to form a junction
|
||||
constexpr float JunctionTolerance = 150.0f;
|
||||
|
||||
// Detect junctions for all pairs
|
||||
for (int32 i = 0; i < AllSplines.Num(); ++i)
|
||||
{
|
||||
for (int32 j = i + 1; j < AllSplines.Num(); ++j)
|
||||
{
|
||||
if (AllSplines[i] && AllSplines[j])
|
||||
{
|
||||
DetectJunctions(AllSplines[i], AllSplines[j], JunctionTolerance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort junctions by distance along spline for each path
|
||||
for (APS_AI_Behavior_SplinePath* Spline : AllSplines)
|
||||
{
|
||||
if (Spline)
|
||||
{
|
||||
Spline->Junctions.Sort([](const FPS_AI_Behavior_SplineJunction& A,
|
||||
const FPS_AI_Behavior_SplineJunction& B)
|
||||
{
|
||||
return A.DistanceOnThisSpline < B.DistanceOnThisSpline;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Log, TEXT("SplineNetwork: %d junctions detected."), TotalJunctions);
|
||||
}
|
||||
|
||||
void UPS_AI_Behavior_SplineNetwork::DetectJunctions(
|
||||
APS_AI_Behavior_SplinePath* SplineA,
|
||||
APS_AI_Behavior_SplinePath* SplineB,
|
||||
float Tolerance)
|
||||
{
|
||||
if (!SplineA || !SplineB) return;
|
||||
if (!SplineA->SplineComp || !SplineB->SplineComp) return;
|
||||
|
||||
const float LengthA = SplineA->GetSplineLength();
|
||||
const float LengthB = SplineB->GetSplineLength();
|
||||
|
||||
if (LengthA <= 0.0f || LengthB <= 0.0f) return;
|
||||
|
||||
// Sample SplineA at regular intervals and check proximity to SplineB
|
||||
const float SampleStep = FMath::Max(50.0f, LengthA / 200.0f); // At least every 50cm
|
||||
const float ToleranceSq = Tolerance * Tolerance;
|
||||
|
||||
// Track the last junction distance to avoid duplicates (merge nearby junctions)
|
||||
float LastJunctionDistA = -Tolerance * 3.0f;
|
||||
|
||||
for (float DistA = 0.0f; DistA <= LengthA; DistA += SampleStep)
|
||||
{
|
||||
// Skip if too close to last detected junction
|
||||
if (DistA - LastJunctionDistA < Tolerance * 2.0f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const FVector PointA = SplineA->GetWorldLocationAtDistance(DistA);
|
||||
|
||||
// Find closest point on SplineB
|
||||
float DistB = 0.0f;
|
||||
FVector PointB = FVector::ZeroVector;
|
||||
const float Gap = SplineB->GetClosestPointOnSpline(PointA, DistB, PointB);
|
||||
|
||||
if (Gap <= Tolerance)
|
||||
{
|
||||
// Found a junction!
|
||||
const FVector JunctionLoc = (PointA + PointB) * 0.5f;
|
||||
|
||||
// Add junction to SplineA
|
||||
FPS_AI_Behavior_SplineJunction JunctionOnA;
|
||||
JunctionOnA.OtherSpline = SplineB;
|
||||
JunctionOnA.DistanceOnThisSpline = DistA;
|
||||
JunctionOnA.DistanceOnOtherSpline = DistB;
|
||||
JunctionOnA.WorldLocation = JunctionLoc;
|
||||
SplineA->Junctions.Add(JunctionOnA);
|
||||
|
||||
// Add mirror junction to SplineB
|
||||
FPS_AI_Behavior_SplineJunction JunctionOnB;
|
||||
JunctionOnB.OtherSpline = SplineA;
|
||||
JunctionOnB.DistanceOnThisSpline = DistB;
|
||||
JunctionOnB.DistanceOnOtherSpline = DistA;
|
||||
JunctionOnB.WorldLocation = JunctionLoc;
|
||||
SplineB->Junctions.Add(JunctionOnB);
|
||||
|
||||
++TotalJunctions;
|
||||
LastJunctionDistA = DistA;
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Verbose,
|
||||
TEXT("SplineNetwork: Junction at (%.0f, %.0f, %.0f) between '%s' (d=%.0f) and '%s' (d=%.0f), gap=%.1fcm"),
|
||||
JunctionLoc.X, JunctionLoc.Y, JunctionLoc.Z,
|
||||
*SplineA->GetName(), DistA,
|
||||
*SplineB->GetName(), DistB,
|
||||
Gap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Queries ────────────────────────────────────────────────────────────────
|
||||
|
||||
bool UPS_AI_Behavior_SplineNetwork::FindClosestSpline(
|
||||
const FVector& WorldLocation, EPS_AI_Behavior_NPCType NPCType,
|
||||
float MaxDistance, APS_AI_Behavior_SplinePath*& OutSpline, float& OutDistance) const
|
||||
{
|
||||
OutSpline = nullptr;
|
||||
float BestGap = MaxDistance;
|
||||
|
||||
for (APS_AI_Behavior_SplinePath* Spline : AllSplines)
|
||||
{
|
||||
if (!Spline || !Spline->IsAccessibleTo(NPCType))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float Dist = 0.0f;
|
||||
FVector ClosestPoint;
|
||||
const float Gap = Spline->GetClosestPointOnSpline(WorldLocation, Dist, ClosestPoint);
|
||||
|
||||
if (Gap < BestGap)
|
||||
{
|
||||
BestGap = Gap;
|
||||
OutSpline = Spline;
|
||||
OutDistance = Dist;
|
||||
}
|
||||
}
|
||||
|
||||
return OutSpline != nullptr;
|
||||
}
|
||||
|
||||
TArray<APS_AI_Behavior_SplinePath*> UPS_AI_Behavior_SplineNetwork::GetSplinesForCategory(
|
||||
EPS_AI_Behavior_NPCType Category) const
|
||||
{
|
||||
TArray<APS_AI_Behavior_SplinePath*> Result;
|
||||
for (APS_AI_Behavior_SplinePath* Spline : AllSplines)
|
||||
{
|
||||
if (Spline && Spline->IsAccessibleTo(Category))
|
||||
{
|
||||
Result.Add(Spline);
|
||||
}
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
APS_AI_Behavior_SplinePath* UPS_AI_Behavior_SplineNetwork::ChooseSplineAtJunction(
|
||||
APS_AI_Behavior_SplinePath* CurrentSpline, int32 JunctionIndex,
|
||||
EPS_AI_Behavior_NPCType NPCType,
|
||||
const FVector& ThreatLocation, float CautionScore) const
|
||||
{
|
||||
if (!CurrentSpline || !CurrentSpline->Junctions.IsValidIndex(JunctionIndex))
|
||||
{
|
||||
return CurrentSpline;
|
||||
}
|
||||
|
||||
const FPS_AI_Behavior_SplineJunction& Junction = CurrentSpline->Junctions[JunctionIndex];
|
||||
APS_AI_Behavior_SplinePath* OtherSpline = Junction.OtherSpline.Get();
|
||||
|
||||
// If other spline is invalid or not accessible, stay
|
||||
if (!OtherSpline || !OtherSpline->IsAccessibleTo(NPCType))
|
||||
{
|
||||
return CurrentSpline;
|
||||
}
|
||||
|
||||
// Score each option
|
||||
float CurrentScore = 0.0f;
|
||||
float OtherScore = 0.0f;
|
||||
|
||||
// Priority
|
||||
CurrentScore += CurrentSpline->Priority * 10.0f;
|
||||
OtherScore += OtherSpline->Priority * 10.0f;
|
||||
|
||||
// Randomness for natural behavior (less random if disciplined)
|
||||
const float RandomRange = FMath::Lerp(30.0f, 5.0f, CautionScore);
|
||||
CurrentScore += FMath::RandRange(-RandomRange, RandomRange);
|
||||
OtherScore += FMath::RandRange(-RandomRange, RandomRange);
|
||||
|
||||
// Threat avoidance (if threat is present)
|
||||
if (!ThreatLocation.IsZero())
|
||||
{
|
||||
const FVector JunctionLoc = Junction.WorldLocation;
|
||||
|
||||
// How far along each spline leads away from threat
|
||||
// Sample a point ahead on each spline
|
||||
const float SampleAhead = 500.0f;
|
||||
|
||||
const float CurrentDist = Junction.DistanceOnThisSpline;
|
||||
const float CurrentLen = CurrentSpline->GetSplineLength();
|
||||
const FVector CurrentAhead = CurrentSpline->GetWorldLocationAtDistance(
|
||||
FMath::Min(CurrentDist + SampleAhead, CurrentLen));
|
||||
const float CurrentDistFromThreat = FVector::Dist(CurrentAhead, ThreatLocation);
|
||||
|
||||
const float OtherDist = Junction.DistanceOnOtherSpline;
|
||||
const float OtherLen = OtherSpline->GetSplineLength();
|
||||
const FVector OtherAhead = OtherSpline->GetWorldLocationAtDistance(
|
||||
FMath::Min(OtherDist + SampleAhead, OtherLen));
|
||||
const float OtherDistFromThreat = FVector::Dist(OtherAhead, ThreatLocation);
|
||||
|
||||
// Cautious NPCs heavily favor paths away from threat
|
||||
const float ThreatWeight = 20.0f * CautionScore;
|
||||
CurrentScore += (CurrentDistFromThreat / 100.0f) * ThreatWeight;
|
||||
OtherScore += (OtherDistFromThreat / 100.0f) * ThreatWeight;
|
||||
}
|
||||
|
||||
// Slight bias toward continuing on current spline (inertia)
|
||||
CurrentScore += 5.0f;
|
||||
|
||||
return (OtherScore > CurrentScore) ? OtherSpline : CurrentSpline;
|
||||
}
|
||||
@ -0,0 +1,141 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#include "PS_AI_Behavior_SplinePath.h"
|
||||
#include "Components/SplineComponent.h"
|
||||
|
||||
APS_AI_Behavior_SplinePath::APS_AI_Behavior_SplinePath()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = false;
|
||||
|
||||
SplineComp = CreateDefaultSubobject<USplineComponent>(TEXT("SplineComp"));
|
||||
RootComponent = SplineComp;
|
||||
|
||||
// Defaults for a nice visible path
|
||||
SplineComp->SetDrawDebug(true);
|
||||
SplineComp->SetUnselectedSplineSegmentColor(FLinearColor::Green);
|
||||
}
|
||||
|
||||
void APS_AI_Behavior_SplinePath::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
UpdateSplineVisualization();
|
||||
}
|
||||
|
||||
bool APS_AI_Behavior_SplinePath::IsAccessibleTo(EPS_AI_Behavior_NPCType NPCType) const
|
||||
{
|
||||
// Any spline → accessible to all
|
||||
if (SplineCategory == EPS_AI_Behavior_NPCType::Any)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Exact match
|
||||
if (SplineCategory == NPCType)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Protectors can also use Civilian splines (allies)
|
||||
if (SplineCategory == EPS_AI_Behavior_NPCType::Civilian
|
||||
&& NPCType == EPS_AI_Behavior_NPCType::Protector)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
float APS_AI_Behavior_SplinePath::GetClosestPointOnSpline(
|
||||
const FVector& WorldLocation, float& OutDistance, FVector& OutWorldPoint) const
|
||||
{
|
||||
if (!SplineComp) return MAX_FLT;
|
||||
|
||||
const float InputKey = SplineComp->FindInputKeyClosestToWorldLocation(WorldLocation);
|
||||
OutDistance = SplineComp->GetDistanceAlongSplineAtSplineInputKey(InputKey);
|
||||
OutWorldPoint = SplineComp->GetLocationAtDistanceAlongSpline(OutDistance, ESplineCoordinateSpace::World);
|
||||
|
||||
return FVector::Dist(WorldLocation, OutWorldPoint);
|
||||
}
|
||||
|
||||
TArray<FPS_AI_Behavior_SplineJunction> APS_AI_Behavior_SplinePath::GetUpcomingJunctions(
|
||||
float CurrentDistance, float LookAheadDist, bool bForward) const
|
||||
{
|
||||
TArray<FPS_AI_Behavior_SplineJunction> Result;
|
||||
|
||||
for (const FPS_AI_Behavior_SplineJunction& J : Junctions)
|
||||
{
|
||||
if (bForward)
|
||||
{
|
||||
if (J.DistanceOnThisSpline > CurrentDistance
|
||||
&& J.DistanceOnThisSpline <= CurrentDistance + LookAheadDist)
|
||||
{
|
||||
Result.Add(J);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (J.DistanceOnThisSpline < CurrentDistance
|
||||
&& J.DistanceOnThisSpline >= CurrentDistance - LookAheadDist)
|
||||
{
|
||||
Result.Add(J);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Result;
|
||||
}
|
||||
|
||||
float APS_AI_Behavior_SplinePath::GetSplineLength() const
|
||||
{
|
||||
return SplineComp ? SplineComp->GetSplineLength() : 0.0f;
|
||||
}
|
||||
|
||||
FVector APS_AI_Behavior_SplinePath::GetWorldLocationAtDistance(float Distance) const
|
||||
{
|
||||
if (!SplineComp) return FVector::ZeroVector;
|
||||
return SplineComp->GetLocationAtDistanceAlongSpline(Distance, ESplineCoordinateSpace::World);
|
||||
}
|
||||
|
||||
FRotator APS_AI_Behavior_SplinePath::GetWorldRotationAtDistance(float Distance) const
|
||||
{
|
||||
if (!SplineComp) return FRotator::ZeroRotator;
|
||||
return SplineComp->GetRotationAtDistanceAlongSpline(Distance, ESplineCoordinateSpace::World);
|
||||
}
|
||||
|
||||
void APS_AI_Behavior_SplinePath::UpdateSplineVisualization()
|
||||
{
|
||||
if (!SplineComp) return;
|
||||
|
||||
FLinearColor Color;
|
||||
switch (SplineCategory)
|
||||
{
|
||||
case EPS_AI_Behavior_NPCType::Civilian:
|
||||
Color = FLinearColor::Green;
|
||||
break;
|
||||
case EPS_AI_Behavior_NPCType::Enemy:
|
||||
Color = FLinearColor::Red;
|
||||
break;
|
||||
case EPS_AI_Behavior_NPCType::Protector:
|
||||
Color = FLinearColor(0.2f, 0.4f, 1.0f); // Blue
|
||||
break;
|
||||
case EPS_AI_Behavior_NPCType::Any:
|
||||
default:
|
||||
Color = FLinearColor(1.0f, 0.7f, 0.0f); // Orange
|
||||
break;
|
||||
}
|
||||
|
||||
SplineComp->SetUnselectedSplineSegmentColor(Color);
|
||||
SplineComp->SetSelectedSplineSegmentColor(FLinearColor::White);
|
||||
}
|
||||
|
||||
#if WITH_EDITOR
|
||||
void APS_AI_Behavior_SplinePath::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
|
||||
{
|
||||
Super::PostEditChangeProperty(PropertyChangedEvent);
|
||||
|
||||
if (PropertyChangedEvent.GetPropertyName() == GET_MEMBER_NAME_CHECKED(APS_AI_Behavior_SplinePath, SplineCategory))
|
||||
{
|
||||
UpdateSplineVisualization();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@ -0,0 +1,48 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "BehaviorTree/BTDecorator.h"
|
||||
#include "PS_AI_Behavior_Definitions.h"
|
||||
#include "PS_AI_Behavior_BTDecorator_CheckTrait.generated.h"
|
||||
|
||||
/** Comparison operator for trait checks. */
|
||||
UENUM(BlueprintType)
|
||||
enum class EPS_AI_Behavior_ComparisonOp : uint8
|
||||
{
|
||||
GreaterThan UMETA(DisplayName = ">"),
|
||||
GreaterOrEqual UMETA(DisplayName = ">="),
|
||||
LessThan UMETA(DisplayName = "<"),
|
||||
LessOrEqual UMETA(DisplayName = "<="),
|
||||
Equal UMETA(DisplayName = "=="),
|
||||
};
|
||||
|
||||
/**
|
||||
* BT Decorator: Checks a personality trait against a threshold.
|
||||
* Use to gate branches: e.g. "Only attack if Courage > 0.5".
|
||||
*/
|
||||
UCLASS(meta = (DisplayName = "PS AI: Check Trait"))
|
||||
class PS_AI_BEHAVIOR_API UPS_AI_Behavior_BTDecorator_CheckTrait : public UBTDecorator
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPS_AI_Behavior_BTDecorator_CheckTrait();
|
||||
|
||||
/** Which personality axis to check. */
|
||||
UPROPERTY(EditAnywhere, Category = "Trait Check")
|
||||
EPS_AI_Behavior_TraitAxis TraitAxis = EPS_AI_Behavior_TraitAxis::Courage;
|
||||
|
||||
/** Comparison operator. */
|
||||
UPROPERTY(EditAnywhere, Category = "Trait Check")
|
||||
EPS_AI_Behavior_ComparisonOp Comparison = EPS_AI_Behavior_ComparisonOp::GreaterThan;
|
||||
|
||||
/** Threshold value to compare against. */
|
||||
UPROPERTY(EditAnywhere, Category = "Trait Check", meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
||||
float Threshold = 0.5f;
|
||||
|
||||
protected:
|
||||
virtual bool CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const override;
|
||||
virtual FString GetStaticDescription() const override;
|
||||
};
|
||||
@ -0,0 +1,26 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "BehaviorTree/BTService.h"
|
||||
#include "PS_AI_Behavior_BTService_EvaluateReaction.generated.h"
|
||||
|
||||
/**
|
||||
* BT Service: Evaluates the NPC's reaction based on personality traits and threat level.
|
||||
* Calls PersonalityComponent::ApplyReaction() and writes the resulting state to the Blackboard.
|
||||
*
|
||||
* Should be placed alongside or below BTService_UpdateThreat in the tree.
|
||||
*/
|
||||
UCLASS(meta = (DisplayName = "PS AI: Evaluate Reaction"))
|
||||
class PS_AI_BEHAVIOR_API UPS_AI_Behavior_BTService_EvaluateReaction : public UBTService
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPS_AI_Behavior_BTService_EvaluateReaction();
|
||||
|
||||
protected:
|
||||
virtual void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
|
||||
virtual FString GetStaticDescription() const override;
|
||||
};
|
||||
@ -0,0 +1,28 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "BehaviorTree/BTService.h"
|
||||
#include "PS_AI_Behavior_BTService_UpdateThreat.generated.h"
|
||||
|
||||
/**
|
||||
* BT Service: Updates threat information in the Blackboard.
|
||||
* Queries PerceptionComponent for the highest threat actor and threat level.
|
||||
* Writes: BB_ThreatActor, BB_ThreatLocation, BB_ThreatLevel.
|
||||
* Also updates PersonalityComponent::PerceivedThreatLevel.
|
||||
*
|
||||
* Place on the root node of any behavior tree that needs threat awareness.
|
||||
*/
|
||||
UCLASS(meta = (DisplayName = "PS AI: Update Threat"))
|
||||
class PS_AI_BEHAVIOR_API UPS_AI_Behavior_BTService_UpdateThreat : public UBTService
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPS_AI_Behavior_BTService_UpdateThreat();
|
||||
|
||||
protected:
|
||||
virtual void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
|
||||
virtual FString GetStaticDescription() const override;
|
||||
};
|
||||
@ -0,0 +1,35 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "BehaviorTree/BTTaskNode.h"
|
||||
#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.
|
||||
*/
|
||||
UCLASS(meta = (DisplayName = "PS AI: Attack"))
|
||||
class PS_AI_BEHAVIOR_API UPS_AI_Behavior_BTTask_Attack : public UBTTaskNode
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPS_AI_Behavior_BTTask_Attack();
|
||||
|
||||
protected:
|
||||
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
|
||||
virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
|
||||
virtual EBTNodeResult::Type AbortTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
|
||||
virtual FString GetStaticDescription() const override;
|
||||
|
||||
private:
|
||||
struct FAttackMemory
|
||||
{
|
||||
bool bMovingToTarget = false;
|
||||
};
|
||||
|
||||
virtual uint16 GetInstanceMemorySize() const override { return sizeof(FAttackMemory); }
|
||||
};
|
||||
@ -0,0 +1,50 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "BehaviorTree/BTTaskNode.h"
|
||||
#include "PS_AI_Behavior_BTTask_FindAndFollowSpline.generated.h"
|
||||
|
||||
/**
|
||||
* BT Task: Find the nearest accessible spline and start following it.
|
||||
* Uses the SplineNetwork subsystem to find the closest spline matching the NPC's type.
|
||||
* Then activates the SplineFollowerComponent.
|
||||
*
|
||||
* Succeeds immediately after starting — use BTTask_FollowSpline to actually follow.
|
||||
* Or use this as a setup node in a Sequence before BTTask_FollowSpline.
|
||||
*/
|
||||
UCLASS(meta = (DisplayName = "PS AI: Find & Start Spline"))
|
||||
class PS_AI_BEHAVIOR_API UPS_AI_Behavior_BTTask_FindAndFollowSpline : public UBTTaskNode
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPS_AI_Behavior_BTTask_FindAndFollowSpline();
|
||||
|
||||
/** Maximum distance to search for a spline (cm). */
|
||||
UPROPERTY(EditAnywhere, Category = "Spline", meta = (ClampMin = "100.0"))
|
||||
float MaxSearchDistance = 3000.0f;
|
||||
|
||||
/** If true, move toward the closest spline point before starting. */
|
||||
UPROPERTY(EditAnywhere, Category = "Spline")
|
||||
bool bWalkToSpline = true;
|
||||
|
||||
/** Acceptance radius for reaching the spline starting point (cm). */
|
||||
UPROPERTY(EditAnywhere, Category = "Spline", meta = (ClampMin = "10.0", EditCondition = "bWalkToSpline"))
|
||||
float AcceptanceRadius = 100.0f;
|
||||
|
||||
protected:
|
||||
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
|
||||
virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
|
||||
virtual EBTNodeResult::Type AbortTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
|
||||
virtual FString GetStaticDescription() const override;
|
||||
|
||||
private:
|
||||
struct FFindSplineMemory
|
||||
{
|
||||
bool bMovingToSpline = false;
|
||||
};
|
||||
|
||||
virtual uint16 GetInstanceMemorySize() const override { return sizeof(FFindSplineMemory); }
|
||||
};
|
||||
@ -0,0 +1,93 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "BehaviorTree/BTTaskNode.h"
|
||||
#include "PS_AI_Behavior_Definitions.h"
|
||||
#include "PS_AI_Behavior_BTTask_FindCover.generated.h"
|
||||
|
||||
class APS_AI_Behavior_CoverPoint;
|
||||
|
||||
/**
|
||||
* BT Task: Find a cover position and navigate to it.
|
||||
*
|
||||
* First checks for manually placed CoverPoint actors in range.
|
||||
* If none found (or bUseManualPointsOnly is false), falls back to
|
||||
* procedural raycast-based cover finding.
|
||||
*
|
||||
* Writes CoverLocation and CoverPoint to the Blackboard on success.
|
||||
*/
|
||||
UCLASS(meta = (DisplayName = "PS AI: Find Cover"))
|
||||
class PS_AI_BEHAVIOR_API UPS_AI_Behavior_BTTask_FindCover : public UBTTaskNode
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPS_AI_Behavior_BTTask_FindCover();
|
||||
|
||||
/** Search radius around the NPC for cover candidates (cm). */
|
||||
UPROPERTY(EditAnywhere, Category = "Cover", meta = (ClampMin = "200.0"))
|
||||
float SearchRadius = 1500.0f;
|
||||
|
||||
/** Number of procedural candidate points to evaluate. */
|
||||
UPROPERTY(EditAnywhere, Category = "Cover|Procedural", meta = (ClampMin = "4", ClampMax = "32"))
|
||||
int32 NumCandidates = 12;
|
||||
|
||||
/** Acceptance radius for reaching cover (cm). */
|
||||
UPROPERTY(EditAnywhere, Category = "Cover", meta = (ClampMin = "10.0"))
|
||||
float AcceptanceRadius = 80.0f;
|
||||
|
||||
/** Minimum height of geometry considered as cover (cm). */
|
||||
UPROPERTY(EditAnywhere, Category = "Cover|Procedural", meta = (ClampMin = "30.0"))
|
||||
float MinCoverHeight = 90.0f;
|
||||
|
||||
/**
|
||||
* Cover point type to search for (Cover for enemies, HidingSpot for civilians).
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, Category = "Cover|Manual Points")
|
||||
EPS_AI_Behavior_CoverPointType CoverPointType = EPS_AI_Behavior_CoverPointType::Cover;
|
||||
|
||||
/**
|
||||
* Bonus score added to manual CoverPoints over procedural candidates.
|
||||
* Higher = manual points are strongly preferred.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, Category = "Cover|Manual Points", meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
||||
float ManualPointBonus = 0.3f;
|
||||
|
||||
/**
|
||||
* If true, only use manually placed CoverPoints — never procedural.
|
||||
* If false (default), manual points are preferred but procedural is fallback.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, Category = "Cover|Manual Points")
|
||||
bool bUseManualPointsOnly = false;
|
||||
|
||||
protected:
|
||||
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
|
||||
virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
|
||||
virtual EBTNodeResult::Type AbortTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
|
||||
virtual FString GetStaticDescription() const override;
|
||||
|
||||
private:
|
||||
struct FCoverMemory
|
||||
{
|
||||
bool bMoveRequested = false;
|
||||
};
|
||||
|
||||
virtual uint16 GetInstanceMemorySize() const override { return sizeof(FCoverMemory); }
|
||||
|
||||
/**
|
||||
* Evaluate cover quality at a given position.
|
||||
* Returns 0.0 (bad) to 1.0 (excellent cover).
|
||||
*/
|
||||
float EvaluateCoverQuality(const UWorld* World, const FVector& CandidatePos,
|
||||
const FVector& ThreatLoc, const FVector& NpcLoc) const;
|
||||
|
||||
/**
|
||||
* Search for the best manual CoverPoint in range.
|
||||
* Returns the best point and its score, or nullptr if none found.
|
||||
*/
|
||||
APS_AI_Behavior_CoverPoint* FindBestManualCoverPoint(
|
||||
const UWorld* World, const FVector& NpcLoc, const FVector& ThreatLoc,
|
||||
EPS_AI_Behavior_NPCType NPCType, float& OutScore) const;
|
||||
};
|
||||
@ -0,0 +1,50 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "BehaviorTree/BTTaskNode.h"
|
||||
#include "PS_AI_Behavior_BTTask_FleeFrom.generated.h"
|
||||
|
||||
/**
|
||||
* BT Task: Flee away from the current threat.
|
||||
* Finds a point in the opposite direction of ThreatLocation and navigates to it.
|
||||
* Can optionally use an EQS query for smarter flee-point selection.
|
||||
*/
|
||||
UCLASS(meta = (DisplayName = "PS AI: Flee From Threat"))
|
||||
class PS_AI_BEHAVIOR_API UPS_AI_Behavior_BTTask_FleeFrom : public UBTTaskNode
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPS_AI_Behavior_BTTask_FleeFrom();
|
||||
|
||||
/** Minimum flee distance from threat (cm). */
|
||||
UPROPERTY(EditAnywhere, Category = "Flee", meta = (ClampMin = "200.0"))
|
||||
float MinFleeDistance = 1000.0f;
|
||||
|
||||
/** Maximum flee distance from threat (cm). */
|
||||
UPROPERTY(EditAnywhere, Category = "Flee", meta = (ClampMin = "500.0"))
|
||||
float MaxFleeDistance = 2500.0f;
|
||||
|
||||
/** Acceptance radius for the flee destination (cm). */
|
||||
UPROPERTY(EditAnywhere, Category = "Flee", meta = (ClampMin = "10.0"))
|
||||
float AcceptanceRadius = 150.0f;
|
||||
|
||||
protected:
|
||||
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
|
||||
virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
|
||||
virtual EBTNodeResult::Type AbortTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
|
||||
virtual FString GetStaticDescription() const override;
|
||||
|
||||
private:
|
||||
struct FFleeMemory
|
||||
{
|
||||
bool bMoveRequested = false;
|
||||
};
|
||||
|
||||
virtual uint16 GetInstanceMemorySize() const override { return sizeof(FFleeMemory); }
|
||||
|
||||
/** Find a navmesh-projected point away from the threat. */
|
||||
bool FindFleePoint(const FVector& Origin, const FVector& ThreatLoc, FVector& OutFleePoint) const;
|
||||
};
|
||||
@ -0,0 +1,56 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "BehaviorTree/BTTaskNode.h"
|
||||
#include "PS_AI_Behavior_BTTask_FollowSpline.generated.h"
|
||||
|
||||
/**
|
||||
* BT Task: Follow the current spline path.
|
||||
* Uses the SplineFollowerComponent on the Pawn.
|
||||
*
|
||||
* This task runs InProgress while the NPC moves along the spline.
|
||||
* It succeeds when the end of the spline is reached (if not looping/reversing).
|
||||
* It can be aborted to interrupt spline movement.
|
||||
*
|
||||
* To start on a specific spline, use BTTask_FindAndFollowSpline first,
|
||||
* or set CurrentSpline via Blueprint/code before this task runs.
|
||||
*/
|
||||
UCLASS(meta = (DisplayName = "PS AI: Follow Spline"))
|
||||
class PS_AI_BEHAVIOR_API UPS_AI_Behavior_BTTask_FollowSpline : public UBTTaskNode
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPS_AI_Behavior_BTTask_FollowSpline();
|
||||
|
||||
/**
|
||||
* Maximum time (seconds) this task will follow the spline before succeeding.
|
||||
* 0 = no time limit (run until end of spline or abort).
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, Category = "Spline", meta = (ClampMin = "0.0"))
|
||||
float MaxFollowTime = 0.0f;
|
||||
|
||||
/**
|
||||
* If true, picks a random direction when starting.
|
||||
* If false, continues in the current direction.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, Category = "Spline")
|
||||
bool bRandomDirection = false;
|
||||
|
||||
protected:
|
||||
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
|
||||
virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
|
||||
virtual EBTNodeResult::Type AbortTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
|
||||
virtual FString GetStaticDescription() const override;
|
||||
|
||||
private:
|
||||
struct FFollowMemory
|
||||
{
|
||||
float Elapsed = 0.0f;
|
||||
bool bEndReached = false;
|
||||
};
|
||||
|
||||
virtual uint16 GetInstanceMemorySize() const override { return sizeof(FFollowMemory); }
|
||||
};
|
||||
@ -0,0 +1,52 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "BehaviorTree/BTTaskNode.h"
|
||||
#include "PS_AI_Behavior_BTTask_Patrol.generated.h"
|
||||
|
||||
/**
|
||||
* BT Task: Navigate to the next patrol waypoint.
|
||||
* Reads PatrolIndex from Blackboard, navigates to the corresponding point in
|
||||
* the AIController's PatrolPoints array, then increments the index (cyclic).
|
||||
*
|
||||
* Optional random wait at each waypoint.
|
||||
*/
|
||||
UCLASS(meta = (DisplayName = "PS AI: Patrol"))
|
||||
class PS_AI_BEHAVIOR_API UPS_AI_Behavior_BTTask_Patrol : public UBTTaskNode
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPS_AI_Behavior_BTTask_Patrol();
|
||||
|
||||
/** Acceptance radius for reaching a waypoint (cm). */
|
||||
UPROPERTY(EditAnywhere, Category = "Patrol", meta = (ClampMin = "10.0"))
|
||||
float AcceptanceRadius = 100.0f;
|
||||
|
||||
/** Minimum wait time at each waypoint (seconds). */
|
||||
UPROPERTY(EditAnywhere, Category = "Patrol", meta = (ClampMin = "0.0"))
|
||||
float MinWaitTime = 1.0f;
|
||||
|
||||
/** Maximum wait time at each waypoint (seconds). */
|
||||
UPROPERTY(EditAnywhere, Category = "Patrol", meta = (ClampMin = "0.0"))
|
||||
float MaxWaitTime = 4.0f;
|
||||
|
||||
protected:
|
||||
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
|
||||
virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
|
||||
virtual EBTNodeResult::Type AbortTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
|
||||
virtual FString GetStaticDescription() const override;
|
||||
|
||||
private:
|
||||
/** Per-instance memory. */
|
||||
struct FPatrolMemory
|
||||
{
|
||||
float WaitRemaining = 0.0f;
|
||||
bool bIsWaiting = false;
|
||||
bool bMoveRequested = false;
|
||||
};
|
||||
|
||||
virtual uint16 GetInstanceMemorySize() const override { return sizeof(FPatrolMemory); }
|
||||
};
|
||||
@ -0,0 +1,22 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "EnvironmentQuery/EnvQueryContext.h"
|
||||
#include "PS_AI_Behavior_EQSContext_Threat.generated.h"
|
||||
|
||||
/**
|
||||
* EQS Context: Returns the current threat actor (or its last known location).
|
||||
* Reads from the Blackboard keys ThreatActor / ThreatLocation.
|
||||
* Use in EQS queries as the "Threat" context for distance/visibility tests.
|
||||
*/
|
||||
UCLASS(meta = (DisplayName = "PS AI: Threat"))
|
||||
class PS_AI_BEHAVIOR_API UPS_AI_Behavior_EQSContext_Threat : public UEnvQueryContext
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
virtual void ProvideContext(FEnvQueryInstance& QueryInstance,
|
||||
FEnvQueryContextData& ContextData) const override;
|
||||
};
|
||||
@ -0,0 +1,41 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "EnvironmentQuery/EnvQueryGenerator.h"
|
||||
#include "PS_AI_Behavior_Definitions.h"
|
||||
#include "PS_AI_Behavior_EQSGenerator_CoverPoints.generated.h"
|
||||
|
||||
/**
|
||||
* EQS Generator: returns all CoverPoint actors in the level as query items.
|
||||
* Filters by: type (Cover/HidingSpot), NPC type accessibility, availability (HasRoom),
|
||||
* and max distance from querier.
|
||||
*
|
||||
* Use with EQSTest_CoverQuality for scoring, or with standard distance/trace tests.
|
||||
*/
|
||||
UCLASS(meta = (DisplayName = "PS AI: Cover Points"))
|
||||
class PS_AI_BEHAVIOR_API UPS_AI_Behavior_EQSGenerator_CoverPoints : public UEnvQueryGenerator
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPS_AI_Behavior_EQSGenerator_CoverPoints();
|
||||
|
||||
/** Filter by cover point type. */
|
||||
UPROPERTY(EditDefaultsOnly, Category = "Generator")
|
||||
EPS_AI_Behavior_CoverPointType PointTypeFilter = EPS_AI_Behavior_CoverPointType::Cover;
|
||||
|
||||
/** Maximum distance from querier to include a cover point (cm). */
|
||||
UPROPERTY(EditDefaultsOnly, Category = "Generator", meta = (ClampMin = "100.0"))
|
||||
float MaxDistance = 3000.0f;
|
||||
|
||||
/** Only include points that have room for another occupant. */
|
||||
UPROPERTY(EditDefaultsOnly, Category = "Generator")
|
||||
bool bOnlyAvailable = true;
|
||||
|
||||
protected:
|
||||
virtual void GenerateItems(FEnvQueryInstance& QueryInstance) const override;
|
||||
virtual FText GetDescriptionTitle() const override;
|
||||
virtual FText GetDescriptionDetails() const override;
|
||||
};
|
||||
@ -0,0 +1,40 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "EnvironmentQuery/EnvQueryTest.h"
|
||||
#include "PS_AI_Behavior_EQSTest_CoverQuality.generated.h"
|
||||
|
||||
/**
|
||||
* EQS Test: Evaluates how well a candidate point provides cover from a threat context.
|
||||
* Performs raycasts at multiple heights to assess visual concealment.
|
||||
* Higher score = better cover.
|
||||
*
|
||||
* Use with EQSContext_Threat as the context for the "threat from" parameter.
|
||||
*/
|
||||
UCLASS(meta = (DisplayName = "PS AI: Cover Quality"))
|
||||
class PS_AI_BEHAVIOR_API UPS_AI_Behavior_EQSTest_CoverQuality : public UEnvQueryTest
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPS_AI_Behavior_EQSTest_CoverQuality();
|
||||
|
||||
/** Number of raycasts from candidate to threat at different heights. */
|
||||
UPROPERTY(EditDefaultsOnly, Category = "Cover", meta = (ClampMin = "1", ClampMax = "5"))
|
||||
int32 NumTraceHeights = 3;
|
||||
|
||||
/** Minimum height for the lowest trace (cm above ground). */
|
||||
UPROPERTY(EditDefaultsOnly, Category = "Cover", meta = (ClampMin = "10.0"))
|
||||
float MinTraceHeight = 50.0f;
|
||||
|
||||
/** Maximum height for the highest trace (cm above ground). */
|
||||
UPROPERTY(EditDefaultsOnly, Category = "Cover", meta = (ClampMin = "50.0"))
|
||||
float MaxTraceHeight = 180.0f;
|
||||
|
||||
protected:
|
||||
virtual void RunTest(FEnvQueryInstance& QueryInstance) const override;
|
||||
virtual FText GetDescriptionTitle() const override;
|
||||
virtual FText GetDescriptionDetails() const override;
|
||||
};
|
||||
@ -0,0 +1,12 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Modules/ModuleManager.h"
|
||||
|
||||
class FPS_AI_BehaviorModule : public IModuleInterface
|
||||
{
|
||||
public:
|
||||
virtual void StartupModule() override;
|
||||
virtual void ShutdownModule() override;
|
||||
};
|
||||
@ -0,0 +1,116 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "AIController.h"
|
||||
#include "GenericTeamAgentInterface.h"
|
||||
#include "PS_AI_Behavior_Definitions.h"
|
||||
#include "PS_AI_Behavior_AIController.generated.h"
|
||||
|
||||
class UPS_AI_Behavior_PerceptionComponent;
|
||||
class UPS_AI_Behavior_PersonalityComponent;
|
||||
class UBehaviorTree;
|
||||
class UBlackboardData;
|
||||
class UBlackboardComponent;
|
||||
|
||||
/**
|
||||
* Base AI Controller for the PS AI Behavior system.
|
||||
* Manages Blackboard setup, Behavior Tree execution, perception, and patrol waypoints.
|
||||
* Automatically discovers PersonalityComponent on the possessed Pawn.
|
||||
*
|
||||
* Optionally detects PS_AI_ConvAgent_ElevenLabsComponent at runtime (no compile dependency).
|
||||
*/
|
||||
UCLASS(BlueprintType, Blueprintable)
|
||||
class PS_AI_BEHAVIOR_API APS_AI_Behavior_AIController : public AAIController
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
APS_AI_Behavior_AIController();
|
||||
|
||||
// ─── Team / Affiliation ─────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Team ID — determines perception affiliation (Enemy/Friendly/Neutral).
|
||||
* Auto-assigned from NPCType at possession if left at 255 (NoTeam):
|
||||
* - Civilian = Team 1
|
||||
* - Enemy = Team 2
|
||||
* - Neutral = 255 (NoTeam → perceived as Neutral by everyone)
|
||||
*
|
||||
* Two NPCs with the SAME Team ID → Friendly (ignored by perception).
|
||||
* Two NPCs with DIFFERENT Team IDs → Enemy (detected by perception).
|
||||
* A NPC with Team ID 255 → Neutral to everyone.
|
||||
*
|
||||
* You can override this in Blueprint or per-instance in the editor.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Team")
|
||||
uint8 TeamId = FGenericTeamId::NoTeam;
|
||||
|
||||
/** Set the team ID at runtime. */
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Team")
|
||||
void SetTeamId(uint8 NewTeamId);
|
||||
|
||||
// ─── IGenericTeamAgentInterface (inherited from AAIController) ────
|
||||
|
||||
virtual FGenericTeamId GetGenericTeamId() const override;
|
||||
virtual void SetGenericTeamId(const FGenericTeamId& InTeamId) override;
|
||||
virtual ETeamAttitude::Type GetTeamAttitudeTowards(const AActor& Other) const override;
|
||||
|
||||
// ─── Configuration ──────────────────────────────────────────────────
|
||||
|
||||
/** Behavior Tree to run. If null, uses the Profile's DefaultBehaviorTree. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Behavior")
|
||||
TObjectPtr<UBehaviorTree> BehaviorTreeAsset;
|
||||
|
||||
/** Blackboard Data asset. If null, a default one is created at runtime. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Behavior")
|
||||
TObjectPtr<UBlackboardData> BlackboardAsset;
|
||||
|
||||
/** Patrol waypoints — set by level designer, spawner, or Blueprint. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Patrol")
|
||||
TArray<FVector> PatrolPoints;
|
||||
|
||||
// ─── Component Access ───────────────────────────────────────────────
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior")
|
||||
UPS_AI_Behavior_PerceptionComponent* GetBehaviorPerception() const { return BehaviorPerception; }
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior")
|
||||
UPS_AI_Behavior_PersonalityComponent* GetPersonalityComponent() const { return PersonalityComp; }
|
||||
|
||||
// ─── Blackboard Helpers ─────────────────────────────────────────────
|
||||
|
||||
/** Write the current behavior state to the Blackboard. */
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Blackboard")
|
||||
void SetBehaviorState(EPS_AI_Behavior_State NewState);
|
||||
|
||||
/** Read the current behavior state from the Blackboard. */
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Blackboard")
|
||||
EPS_AI_Behavior_State GetBehaviorState() const;
|
||||
|
||||
protected:
|
||||
virtual void OnPossess(APawn* InPawn) override;
|
||||
virtual void OnUnPossess() override;
|
||||
|
||||
/** Our custom perception component — created in constructor. */
|
||||
UPROPERTY(VisibleAnywhere, Category = "Components")
|
||||
TObjectPtr<UPS_AI_Behavior_PerceptionComponent> BehaviorPerception;
|
||||
|
||||
/** Cached ref to the Pawn's PersonalityComponent. */
|
||||
UPROPERTY(Transient)
|
||||
TObjectPtr<UPS_AI_Behavior_PersonalityComponent> PersonalityComp;
|
||||
|
||||
private:
|
||||
/** Initialize Blackboard with required keys. */
|
||||
void SetupBlackboard();
|
||||
|
||||
/** Start the Behavior Tree (from asset or profile). */
|
||||
void StartBehavior();
|
||||
|
||||
/**
|
||||
* Attempt to bind to PS_AI_ConvAgent_ElevenLabsComponent if present on the Pawn.
|
||||
* Uses UObject reflection — no compile-time dependency on PS_AI_ConvAgent.
|
||||
*/
|
||||
void TryBindConversationAgent();
|
||||
};
|
||||
@ -0,0 +1,103 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "PS_AI_Behavior_CombatComponent.generated.h"
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnAttackExecuted, AActor*, Target);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnDamageReceived, float, Damage, AActor*, Instigator);
|
||||
|
||||
/**
|
||||
* Manages NPC combat state: attack range, cooldown, damage dealing.
|
||||
*
|
||||
* Replication: CurrentTarget is replicated so clients know who the NPC
|
||||
* is fighting. ExecuteAttack fires a NetMulticast for cosmetic effects.
|
||||
*
|
||||
* Attach to the NPC Pawn alongside PersonalityComponent.
|
||||
*/
|
||||
UCLASS(ClassGroup = "PS AI Behavior", meta = (BlueprintSpawnableComponent, DisplayName = "PS AI Behavior - Combat"))
|
||||
class PS_AI_BEHAVIOR_API UPS_AI_Behavior_CombatComponent : public UActorComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPS_AI_Behavior_CombatComponent();
|
||||
|
||||
// ─── Configuration ──────────────────────────────────────────────────
|
||||
|
||||
/** Maximum distance at which the NPC can attack (cm). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat", meta = (ClampMin = "50.0"))
|
||||
float AttackRange = 200.0f;
|
||||
|
||||
/** Cooldown between attacks (seconds). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat", meta = (ClampMin = "0.1"))
|
||||
float AttackCooldown = 1.5f;
|
||||
|
||||
/** Base damage per attack. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat", meta = (ClampMin = "0.0"))
|
||||
float AttackDamage = 20.0f;
|
||||
|
||||
/** Damage type class for applying damage. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat")
|
||||
TSubclassOf<UDamageType> DamageTypeClass;
|
||||
|
||||
// ─── Runtime State ──────────────────────────────────────────────────
|
||||
|
||||
/** Current attack target — replicated so clients can show targeting visuals. */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Replicated, Category = "Combat|Runtime")
|
||||
TObjectPtr<AActor> CurrentTarget;
|
||||
|
||||
// ─── Delegates ──────────────────────────────────────────────────────
|
||||
|
||||
/** Fired on ALL machines (server + clients) when an attack is executed. */
|
||||
UPROPERTY(BlueprintAssignable, Category = "PS AI Behavior|Combat")
|
||||
FOnAttackExecuted OnAttackExecuted;
|
||||
|
||||
/** Fired on server when damage is received. */
|
||||
UPROPERTY(BlueprintAssignable, Category = "PS AI Behavior|Combat")
|
||||
FOnDamageReceived OnDamageReceived;
|
||||
|
||||
// ─── API ────────────────────────────────────────────────────────────
|
||||
|
||||
/** Whether the NPC can currently attack (cooldown elapsed). Server-only. */
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Combat")
|
||||
bool CanAttack() const;
|
||||
|
||||
/** Whether the target is within attack range. */
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Combat")
|
||||
bool IsInAttackRange(AActor* Target) const;
|
||||
|
||||
/**
|
||||
* Execute an attack on the target. Applies damage (server), triggers
|
||||
* cooldown, and multicasts cosmetic event to all clients.
|
||||
* @return True if the attack was executed.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Combat")
|
||||
bool ExecuteAttack(AActor* Target);
|
||||
|
||||
/**
|
||||
* Called when this NPC takes damage. Updates threat perception.
|
||||
* Hook this into the owning Actor's OnTakeAnyDamage.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Combat")
|
||||
void NotifyDamageReceived(float Damage, AActor* DamageInstigator);
|
||||
|
||||
// ─── Replication ────────────────────────────────────────────────────
|
||||
|
||||
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
|
||||
|
||||
protected:
|
||||
virtual void BeginPlay() override;
|
||||
virtual void TickComponent(float DeltaTime, ELevelTick TickType,
|
||||
FActorComponentTickFunction* ThisTickFunction) override;
|
||||
|
||||
/** Multicast: notify all clients that an attack happened (for VFX, sound, anims). */
|
||||
UFUNCTION(NetMulticast, Unreliable)
|
||||
void Multicast_OnAttackExecuted(AActor* Target);
|
||||
|
||||
private:
|
||||
/** Time remaining before next attack is allowed. Server-only. */
|
||||
float CooldownRemaining = 0.0f;
|
||||
};
|
||||
@ -0,0 +1,131 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "PS_AI_Behavior_Definitions.h"
|
||||
#include "PS_AI_Behavior_CoverPoint.generated.h"
|
||||
|
||||
class UArrowComponent;
|
||||
class UBillboardComponent;
|
||||
|
||||
/**
|
||||
* A manually placed strategic point in the level.
|
||||
*
|
||||
* - **Cover**: positioned behind walls/barricades for enemies in combat.
|
||||
* The arrow shows the direction the NPC will face (toward the threat).
|
||||
*
|
||||
* - **Hiding Spot**: under desks, in closets, behind cars — for panicking civilians.
|
||||
*
|
||||
* Features:
|
||||
* - Occupancy system: only one NPC per point (configurable max).
|
||||
* - Quality score: manually set by the level designer (0.0-1.0).
|
||||
* - Crouch flag: NPC should crouch at this cover.
|
||||
* - Editor: color-coded (blue=Cover, yellow=HidingSpot), arrow shows facing.
|
||||
*/
|
||||
UCLASS(BlueprintType, Blueprintable, meta = (DisplayName = "PS AI Cover Point"))
|
||||
class PS_AI_BEHAVIOR_API APS_AI_Behavior_CoverPoint : public AActor
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
APS_AI_Behavior_CoverPoint();
|
||||
|
||||
// ─── Components ─────────────────────────────────────────────────────
|
||||
|
||||
#if WITH_EDITORONLY_DATA
|
||||
UPROPERTY(VisibleAnywhere, Category = "Components")
|
||||
TObjectPtr<UBillboardComponent> SpriteComp;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, Category = "Components")
|
||||
TObjectPtr<UArrowComponent> ArrowComp;
|
||||
#endif
|
||||
|
||||
// ─── Configuration ──────────────────────────────────────────────────
|
||||
|
||||
/** Type of this point. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cover Point")
|
||||
EPS_AI_Behavior_CoverPointType PointType = EPS_AI_Behavior_CoverPointType::Cover;
|
||||
|
||||
/**
|
||||
* Manual quality score set by the level designer.
|
||||
* 0.0 = poor cover, 1.0 = excellent cover.
|
||||
* Combined with runtime raycast verification.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cover Point", meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
||||
float Quality = 0.7f;
|
||||
|
||||
/** Maximum number of NPCs that can occupy this point simultaneously. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cover Point", meta = (ClampMin = "1", ClampMax = "4"))
|
||||
int32 MaxOccupants = 1;
|
||||
|
||||
/** NPC should crouch when using this cover point. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cover Point")
|
||||
bool bCrouch = true;
|
||||
|
||||
/**
|
||||
* Optional: restrict this point to specific NPC types.
|
||||
* Any = all NPC types can use it. Otherwise, matches the NPC's type.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cover Point")
|
||||
EPS_AI_Behavior_NPCType AllowedNPCType = EPS_AI_Behavior_NPCType::Any;
|
||||
|
||||
/**
|
||||
* Whether this point is currently enabled. Disabled points are ignored.
|
||||
* Useful for scripted scenarios (e.g. barricade destroyed → disable cover).
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cover Point")
|
||||
bool bEnabled = true;
|
||||
|
||||
// ─── Runtime (server-only) ──────────────────────────────────────────
|
||||
|
||||
/** Current occupants. Managed by the BT / EQS. */
|
||||
UPROPERTY(Transient)
|
||||
TArray<TWeakObjectPtr<AActor>> CurrentOccupants;
|
||||
|
||||
// ─── API ────────────────────────────────────────────────────────────
|
||||
|
||||
/** Can this point be used by the given NPC type? */
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Cover")
|
||||
bool IsAccessibleTo(EPS_AI_Behavior_NPCType NPCType) const;
|
||||
|
||||
/** Is there room for one more occupant? */
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Cover")
|
||||
bool HasRoom() const;
|
||||
|
||||
/** Try to claim this point for an NPC. Returns true if successful. */
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Cover")
|
||||
bool Claim(AActor* Occupant);
|
||||
|
||||
/** Release this point (NPC leaves cover). */
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Cover")
|
||||
void Release(AActor* Occupant);
|
||||
|
||||
/** Get the facing direction (forward vector of the actor). */
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Cover")
|
||||
FVector GetCoverDirection() const;
|
||||
|
||||
/**
|
||||
* Evaluate cover quality at runtime with a raycast check against a threat.
|
||||
* Combines manual Quality score with actual line-of-sight blockage.
|
||||
* @param ThreatLocation Where the threat is.
|
||||
* @return Combined score 0.0 to 1.0.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Cover")
|
||||
float EvaluateAgainstThreat(const FVector& ThreatLocation) const;
|
||||
|
||||
/** Enable/disable at runtime (e.g. barricade destroyed). */
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Cover")
|
||||
void SetEnabled(bool bNewEnabled);
|
||||
|
||||
#if WITH_EDITOR
|
||||
virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
private:
|
||||
void UpdateVisualization();
|
||||
};
|
||||
@ -0,0 +1,98 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "PS_AI_Behavior_Definitions.generated.h"
|
||||
|
||||
// ─── Log Category ───────────────────────────────────────────────────────────
|
||||
|
||||
PS_AI_BEHAVIOR_API DECLARE_LOG_CATEGORY_EXTERN(LogPS_AI_Behavior, Log, All);
|
||||
|
||||
// ─── API Macro ──────────────────────────────────────────────────────────────
|
||||
|
||||
// Defined by UBT from module name; redeclare for clarity
|
||||
#ifndef PS_AI_BEHAVIOR_API
|
||||
#define PS_AI_BEHAVIOR_API
|
||||
#endif
|
||||
|
||||
// ─── Enums ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Type of NPC — determines team affiliation, spline access, and default behavior.
|
||||
* Also used on splines to restrict which NPCs can walk on them.
|
||||
* "Any" means accessible to all types (splines only — not a valid NPC type).
|
||||
*/
|
||||
UENUM(BlueprintType)
|
||||
enum class EPS_AI_Behavior_NPCType : uint8
|
||||
{
|
||||
Civilian UMETA(DisplayName = "Civilian", ToolTip = "Non-hostile civilians"),
|
||||
Enemy UMETA(DisplayName = "Enemy", ToolTip = "Hostile NPCs"),
|
||||
Protector UMETA(DisplayName = "Protector", ToolTip = "Police/guards, allied with Civilians"),
|
||||
Any UMETA(DisplayName = "Any", ToolTip = "Splines only: accessible to all types"),
|
||||
};
|
||||
|
||||
/** High-level behavioral state written to the Blackboard. */
|
||||
UENUM(BlueprintType)
|
||||
enum class EPS_AI_Behavior_State : uint8
|
||||
{
|
||||
Idle UMETA(DisplayName = "Idle"),
|
||||
Patrol UMETA(DisplayName = "Patrol"),
|
||||
Alerted UMETA(DisplayName = "Alerted"),
|
||||
Combat UMETA(DisplayName = "Combat"),
|
||||
Fleeing UMETA(DisplayName = "Fleeing"),
|
||||
TakingCover UMETA(DisplayName = "Taking Cover"),
|
||||
Dead UMETA(DisplayName = "Dead"),
|
||||
};
|
||||
|
||||
/**
|
||||
* Target type for combat priority.
|
||||
* Includes Player which is not an NPC type but is a valid target.
|
||||
*/
|
||||
UENUM(BlueprintType)
|
||||
enum class EPS_AI_Behavior_TargetType : uint8
|
||||
{
|
||||
Player UMETA(DisplayName = "Player", ToolTip = "Human-controlled character"),
|
||||
Civilian UMETA(DisplayName = "Civilian", ToolTip = "Non-hostile NPC"),
|
||||
Protector UMETA(DisplayName = "Protector", ToolTip = "Police/guard NPC"),
|
||||
Enemy UMETA(DisplayName = "Enemy", ToolTip = "Hostile NPC (same faction — rare)"),
|
||||
};
|
||||
|
||||
/**
|
||||
* Type of strategic point placed manually in the level.
|
||||
* Cover = enemies use it for tactical combat cover.
|
||||
* HidingSpot = civilians use it to hide when panicking.
|
||||
*/
|
||||
UENUM(BlueprintType)
|
||||
enum class EPS_AI_Behavior_CoverPointType : uint8
|
||||
{
|
||||
Cover UMETA(DisplayName = "Cover", ToolTip = "Tactical cover for enemies (behind walls, barricades)"),
|
||||
HidingSpot UMETA(DisplayName = "Hiding Spot", ToolTip = "Civilian hiding place (under desks, in closets, behind cars)"),
|
||||
};
|
||||
|
||||
/** Personality trait axes — each scored 0.0 (low) to 1.0 (high). */
|
||||
UENUM(BlueprintType)
|
||||
enum class EPS_AI_Behavior_TraitAxis : uint8
|
||||
{
|
||||
Courage UMETA(DisplayName = "Courage", ToolTip = "0 = coward, 1 = fearless"),
|
||||
Aggressivity UMETA(DisplayName = "Aggressivity", ToolTip = "0 = peaceful, 1 = violent"),
|
||||
Loyalty UMETA(DisplayName = "Loyalty", ToolTip = "0 = selfish, 1 = devoted"),
|
||||
Caution UMETA(DisplayName = "Caution", ToolTip = "0 = reckless, 1 = prudent"),
|
||||
Discipline UMETA(DisplayName = "Discipline", ToolTip = "0 = undisciplined, 1 = disciplined"),
|
||||
};
|
||||
|
||||
// ─── Blackboard Key Names ───────────────────────────────────────────────────
|
||||
|
||||
namespace PS_AI_Behavior_BB
|
||||
{
|
||||
inline const FName State = TEXT("BehaviorState");
|
||||
inline const FName ThreatActor = TEXT("ThreatActor");
|
||||
inline const FName ThreatLocation = TEXT("ThreatLocation");
|
||||
inline const FName ThreatLevel = TEXT("ThreatLevel");
|
||||
inline const FName CoverLocation = TEXT("CoverLocation");
|
||||
inline const FName CoverPoint = TEXT("CoverPoint");
|
||||
inline const FName PatrolIndex = TEXT("PatrolIndex");
|
||||
inline const FName HomeLocation = TEXT("HomeLocation");
|
||||
inline const FName CurrentSpline = TEXT("CurrentSpline");
|
||||
inline const FName SplineProgress = TEXT("SplineProgress");
|
||||
}
|
||||
@ -0,0 +1,114 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/Interface.h"
|
||||
#include "PS_AI_Behavior_Definitions.h"
|
||||
#include "PS_AI_Behavior_Interface.generated.h"
|
||||
|
||||
/**
|
||||
* General-purpose interface for the PS AI Behavior plugin.
|
||||
*
|
||||
* Implement this on your Pawn/Character classes so the behavior system can
|
||||
* query and modify NPC identity, hostility, and team affiliation without
|
||||
* any compile-time dependency on your project's class hierarchy.
|
||||
*
|
||||
* Implementable in C++ (BlueprintNativeEvent) or Blueprint (BlueprintImplementableEvent).
|
||||
*
|
||||
* Example C++ implementation on your Character:
|
||||
*
|
||||
* class AMyCharacter : public ACharacter, public IPS_AI_Behavior
|
||||
* {
|
||||
* EPS_AI_Behavior_NPCType MyType = EPS_AI_Behavior_NPCType::Civilian;
|
||||
* bool bHostile = false;
|
||||
*
|
||||
* virtual EPS_AI_Behavior_NPCType GetBehaviorNPCType_Implementation() const override { return MyType; }
|
||||
* virtual void SetBehaviorNPCType_Implementation(EPS_AI_Behavior_NPCType T) override { MyType = T; }
|
||||
* virtual bool IsBehaviorHostile_Implementation() const override { return bHostile; }
|
||||
* virtual void SetBehaviorHostile_Implementation(bool b) override { bHostile = b; }
|
||||
* virtual uint8 GetBehaviorTeamId_Implementation() const override { return bHostile ? 2 : 1; }
|
||||
* };
|
||||
*/
|
||||
UINTERFACE(BlueprintType, Blueprintable, meta = (DisplayName = "PS AI Behavior Interface"))
|
||||
class PS_AI_BEHAVIOR_API UPS_AI_Behavior_Interface : public UInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
};
|
||||
|
||||
class PS_AI_BEHAVIOR_API IPS_AI_Behavior_Interface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
// ─── NPC Type ───────────────────────────────────────────────────────
|
||||
|
||||
/** Get this NPC's type (Civilian, Enemy, Protector). */
|
||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS AI Behavior")
|
||||
EPS_AI_Behavior_NPCType GetBehaviorNPCType() const;
|
||||
|
||||
/** Set this NPC's type. Called by gameplay logic or plugin actions. */
|
||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS AI Behavior")
|
||||
void SetBehaviorNPCType(EPS_AI_Behavior_NPCType NewType);
|
||||
|
||||
// ─── Hostility ──────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Is this NPC currently hostile?
|
||||
* An infiltrated Enemy with IsHostile=false appears as Civilian to the perception system.
|
||||
* When SetHostile(true) is called, the NPC reveals itself and TeamId changes.
|
||||
*/
|
||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS AI Behavior")
|
||||
bool IsBehaviorHostile() const;
|
||||
|
||||
/**
|
||||
* Set hostility state. Typically called by gameplay scripts or ConvAgent actions.
|
||||
* Implementors should update their TeamId accordingly.
|
||||
*/
|
||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS AI Behavior")
|
||||
void SetBehaviorHostile(bool bNewHostile);
|
||||
|
||||
// ─── Team ───────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Get the Team ID for perception affiliation.
|
||||
* Convention: Civilian=1, Enemy=2, Protector=3, NoTeam=255.
|
||||
* Infiltrated enemies return 1 (Civilian) until SetHostile(true).
|
||||
*/
|
||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS AI Behavior")
|
||||
uint8 GetBehaviorTeamId() const;
|
||||
|
||||
// ─── Movement ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Request the Pawn to change its movement speed.
|
||||
* Called by the behavior system when the NPC's state changes
|
||||
* (e.g. panicking civilian runs, cautious enemy crouches slowly).
|
||||
*
|
||||
* The Pawn implements this however it wants — typically by setting
|
||||
* CharacterMovementComponent::MaxWalkSpeed.
|
||||
*
|
||||
* @param NewSpeed Desired walk speed in cm/s.
|
||||
*/
|
||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS AI Behavior")
|
||||
void SetBehaviorMovementSpeed(float NewSpeed);
|
||||
|
||||
/**
|
||||
* Get the Pawn's current movement speed (cm/s).
|
||||
*/
|
||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS AI Behavior")
|
||||
float GetBehaviorMovementSpeed() const;
|
||||
|
||||
/**
|
||||
* Notify the Pawn that the behavioral state changed.
|
||||
* The Pawn can use this to trigger animations, voice lines, VFX, etc.
|
||||
* Called on the server — the Pawn is responsible for replicating
|
||||
* any cosmetic effects if needed.
|
||||
*
|
||||
* @param NewState The new behavioral state.
|
||||
* @param OldState The previous behavioral state.
|
||||
*/
|
||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS AI Behavior")
|
||||
void OnBehaviorStateChanged(EPS_AI_Behavior_State NewState, EPS_AI_Behavior_State OldState);
|
||||
};
|
||||
@ -0,0 +1,73 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Perception/AIPerceptionComponent.h"
|
||||
#include "PS_AI_Behavior_Definitions.h"
|
||||
#include "PS_AI_Behavior_PerceptionComponent.generated.h"
|
||||
|
||||
/**
|
||||
* Pre-configured AI Perception component for the behavior system.
|
||||
* Sets up Sight, Hearing, and Damage senses with defaults from plugin settings.
|
||||
* Provides helpers to query the highest threat and compute a threat level.
|
||||
*
|
||||
* Automatically added by PS_AI_Behavior_AIController — you don't need to add it manually.
|
||||
*/
|
||||
UCLASS(ClassGroup = "PS AI Behavior", meta = (DisplayName = "PS AI Behavior - Perception"))
|
||||
class PS_AI_BEHAVIOR_API UPS_AI_Behavior_PerceptionComponent : public UAIPerceptionComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPS_AI_Behavior_PerceptionComponent();
|
||||
|
||||
// ─── Queries ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Get the actor that represents the highest threat, considering target priority.
|
||||
* Scoring: priority rank (from PersonalityProfile) > damage sense > proximity.
|
||||
*
|
||||
* @param TargetPriority Ordered list of target types (first = highest priority).
|
||||
* If empty, uses default [Protector, Player, Civilian].
|
||||
* @return The most threatening actor, or nullptr if none perceived.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Perception")
|
||||
AActor* GetHighestThreatActor(const TArray<EPS_AI_Behavior_TargetType>& TargetPriority);
|
||||
|
||||
/** Convenience overload — reads priority from the Pawn's PersonalityProfile. */
|
||||
AActor* GetHighestThreatActor();
|
||||
|
||||
/**
|
||||
* Compute an aggregate threat level from all currently perceived hostile stimuli.
|
||||
* Returns 0.0 (no threat) to 1.0+ (extreme danger).
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Perception")
|
||||
float CalculateThreatLevel();
|
||||
|
||||
/**
|
||||
* Get the location of the last known threat stimulus.
|
||||
* @param OutLocation Filled with the threat location if any threat exists.
|
||||
* @return True if a threat was found.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Perception")
|
||||
bool GetThreatLocation(FVector& OutLocation);
|
||||
|
||||
protected:
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
UFUNCTION()
|
||||
void HandlePerceptionUpdated(const TArray<AActor*>& UpdatedActors);
|
||||
|
||||
/**
|
||||
* Classify an actor as a TargetType.
|
||||
* Uses IsPlayerControlled() for Player, IPS_AI_Behavior interface or
|
||||
* PersonalityComponent for NPC type.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Perception")
|
||||
static EPS_AI_Behavior_TargetType ClassifyActor(const AActor* Actor);
|
||||
|
||||
private:
|
||||
/** Configure sight, hearing, and damage senses from plugin settings. */
|
||||
void ConfigureSenses();
|
||||
};
|
||||
@ -0,0 +1,121 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "Net/UnrealNetwork.h"
|
||||
#include "PS_AI_Behavior_Definitions.h"
|
||||
#include "PS_AI_Behavior_PersonalityComponent.generated.h"
|
||||
|
||||
class UPS_AI_Behavior_PersonalityProfile;
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnBehaviorStateChanged, EPS_AI_Behavior_State, OldState, EPS_AI_Behavior_State, NewState);
|
||||
|
||||
/**
|
||||
* Manages an NPC's personality traits at runtime.
|
||||
* Reads from a PersonalityProfile data asset, maintains runtime-modifiable trait scores,
|
||||
* and evaluates the NPC's behavioral reaction to perceived threats.
|
||||
*
|
||||
* Replication: CurrentState and PerceivedThreatLevel are replicated to all clients
|
||||
* so that animations and HUD can reflect the NPC's current behavior.
|
||||
*
|
||||
* Attach to the NPC Pawn/Character.
|
||||
*/
|
||||
UCLASS(ClassGroup = "PS AI Behavior", meta = (BlueprintSpawnableComponent, DisplayName = "PS AI Behavior - Personality"))
|
||||
class PS_AI_BEHAVIOR_API UPS_AI_Behavior_PersonalityComponent : public UActorComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPS_AI_Behavior_PersonalityComponent();
|
||||
|
||||
// ─── Configuration ──────────────────────────────────────────────────
|
||||
|
||||
/** Personality profile data asset. Set in the editor per NPC archetype. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Personality")
|
||||
TObjectPtr<UPS_AI_Behavior_PersonalityProfile> Profile;
|
||||
|
||||
// ─── Runtime State ──────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Runtime trait scores — initialized from Profile at BeginPlay.
|
||||
* Can be modified during gameplay (e.g. NPC becomes more courageous over time).
|
||||
* Server-only: traits drive AI decisions which run on server.
|
||||
*/
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Personality|Runtime")
|
||||
TMap<EPS_AI_Behavior_TraitAxis, float> RuntimeTraits;
|
||||
|
||||
/**
|
||||
* Current perceived threat level (0.0 = safe, 1.0 = maximum danger).
|
||||
* Written by BTService_UpdateThreat on the server.
|
||||
* Replicated for client HUD/debug display.
|
||||
*/
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Replicated, Category = "Personality|Runtime")
|
||||
float PerceivedThreatLevel = 0.0f;
|
||||
|
||||
/**
|
||||
* Current behavioral state — replicated with OnRep to fire delegate on clients.
|
||||
* Only written on the server (by BT or ForceState).
|
||||
*/
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, ReplicatedUsing = OnRep_CurrentState, Category = "Personality|Runtime")
|
||||
EPS_AI_Behavior_State CurrentState = EPS_AI_Behavior_State::Idle;
|
||||
|
||||
// ─── Delegates ──────────────────────────────────────────────────────
|
||||
|
||||
/** Fired when the behavioral state changes (on server AND clients via OnRep). */
|
||||
UPROPERTY(BlueprintAssignable, Category = "PS AI Behavior|Personality")
|
||||
FOnBehaviorStateChanged OnBehaviorStateChanged;
|
||||
|
||||
// ─── API ────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Evaluate the NPC's reaction based on current traits and perceived threat.
|
||||
* Returns the recommended behavioral state. Server-only.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Personality")
|
||||
EPS_AI_Behavior_State EvaluateReaction() const;
|
||||
|
||||
/**
|
||||
* Evaluate and apply the reaction — updates CurrentState and fires delegate if changed.
|
||||
* Server-only: state is replicated to clients via OnRep.
|
||||
* @return The new behavioral state.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Personality")
|
||||
EPS_AI_Behavior_State ApplyReaction();
|
||||
|
||||
/** Get a runtime trait value (returns 0.5 if undefined). */
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Personality")
|
||||
float GetTrait(EPS_AI_Behavior_TraitAxis Axis) const;
|
||||
|
||||
/** Modify a runtime trait by delta, clamped to [0, 1]. Server-only. */
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Personality")
|
||||
void ModifyTrait(EPS_AI_Behavior_TraitAxis Axis, float Delta);
|
||||
|
||||
/** Force a specific state (e.g. from conversation agent action). Server-only. */
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Personality")
|
||||
void ForceState(EPS_AI_Behavior_State NewState);
|
||||
|
||||
/** Get the NPC type from the interface or profile. */
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Personality")
|
||||
EPS_AI_Behavior_NPCType GetNPCType() const;
|
||||
|
||||
// ─── Replication ────────────────────────────────────────────────────
|
||||
|
||||
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
|
||||
|
||||
protected:
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
UFUNCTION()
|
||||
void OnRep_CurrentState(EPS_AI_Behavior_State OldState);
|
||||
|
||||
private:
|
||||
/**
|
||||
* Central handler for state transitions. Called on server when state changes.
|
||||
* - Broadcasts the delegate
|
||||
* - Calls IPS_AI_Behavior::SetBehaviorMovementSpeed on the Pawn
|
||||
* - Calls IPS_AI_Behavior::OnBehaviorStateChanged on the Pawn
|
||||
*/
|
||||
void HandleStateChanged(EPS_AI_Behavior_State OldState, EPS_AI_Behavior_State NewState);
|
||||
};
|
||||
@ -0,0 +1,121 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/DataAsset.h"
|
||||
#include "PS_AI_Behavior_Definitions.h"
|
||||
#include "PS_AI_Behavior_PersonalityProfile.generated.h"
|
||||
|
||||
class UBehaviorTree;
|
||||
|
||||
/**
|
||||
* Data Asset defining an NPC's personality profile.
|
||||
* Contains trait scores, reaction thresholds, and default behavior tree.
|
||||
* Create one per archetype (e.g. "Coward Civilian", "Aggressive Guard").
|
||||
*/
|
||||
UCLASS(BlueprintType)
|
||||
class PS_AI_BEHAVIOR_API UPS_AI_Behavior_PersonalityProfile : public UPrimaryDataAsset
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPS_AI_Behavior_PersonalityProfile();
|
||||
|
||||
// ─── Identity ───────────────────────────────────────────────────────
|
||||
|
||||
/** Human-readable profile name (e.g. "Cowardly Villager"). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Personality")
|
||||
FText ProfileName;
|
||||
|
||||
/** NPC type — determines base behavior tree selection. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Personality")
|
||||
EPS_AI_Behavior_NPCType NPCType = EPS_AI_Behavior_NPCType::Civilian;
|
||||
|
||||
// ─── Trait Scores ───────────────────────────────────────────────────
|
||||
|
||||
/** Personality trait scores. Each axis ranges from 0.0 to 1.0. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Personality|Traits",
|
||||
meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
||||
TMap<EPS_AI_Behavior_TraitAxis, float> TraitScores;
|
||||
|
||||
// ─── Reaction Thresholds ────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Base threat level above which the NPC considers fleeing.
|
||||
* Actual threshold is modulated at runtime by Courage trait.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Personality|Reaction",
|
||||
meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
||||
float FleeThreshold = 0.5f;
|
||||
|
||||
/**
|
||||
* Base threat level above which the NPC engages in combat.
|
||||
* Actual threshold is modulated at runtime by Aggressivity trait.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Personality|Reaction",
|
||||
meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
||||
float AttackThreshold = 0.4f;
|
||||
|
||||
/**
|
||||
* Base threat level above which the NPC becomes alerted (but not yet fleeing/attacking).
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Personality|Reaction",
|
||||
meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
||||
float AlertThreshold = 0.15f;
|
||||
|
||||
// ─── Target Priority (Combat) ───────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Target selection priority for combat, in order of preference.
|
||||
* First entry = highest priority target type.
|
||||
*
|
||||
* Example for a terrorist: [Player, Protector, Civilian]
|
||||
* Example for a thief: [Civilian, Player] (avoids Protectors)
|
||||
* Example for a rival gang: [Enemy, Protector, Player]
|
||||
*
|
||||
* If empty, defaults to: [Protector, Player, Civilian].
|
||||
* Target types not in the list will not be attacked.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Personality|Combat")
|
||||
TArray<EPS_AI_Behavior_TargetType> TargetPriority;
|
||||
|
||||
// ─── Movement Speed per State ──────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Movement speed (cm/s) for each behavioral state.
|
||||
* The behavior system calls IPS_AI_Behavior::SetBehaviorMovementSpeed()
|
||||
* on the Pawn when the state changes.
|
||||
*
|
||||
* States not in this map use DefaultWalkSpeed.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Personality|Movement")
|
||||
TMap<EPS_AI_Behavior_State, float> SpeedPerState;
|
||||
|
||||
/**
|
||||
* Base walk speed (cm/s) used when the current state is not in SpeedPerState.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Personality|Movement", meta = (ClampMin = "0.0"))
|
||||
float DefaultWalkSpeed = 150.0f;
|
||||
|
||||
// ─── Behavior ───────────────────────────────────────────────────────
|
||||
|
||||
/** Default Behavior Tree for this personality archetype. Can be overridden on the AIController. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Behavior")
|
||||
TSoftObjectPtr<UBehaviorTree> DefaultBehaviorTree;
|
||||
|
||||
// ─── Helpers ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Get the score for a given trait axis. Returns 0.5 if the axis is not defined.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Personality")
|
||||
float GetTrait(EPS_AI_Behavior_TraitAxis Axis) const;
|
||||
|
||||
/** Get the speed for a given state. Returns DefaultWalkSpeed if state not in SpeedPerState. */
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Personality")
|
||||
float GetSpeedForState(EPS_AI_Behavior_State State) const;
|
||||
|
||||
/** UPrimaryDataAsset interface */
|
||||
virtual FPrimaryAssetId GetPrimaryAssetId() const override;
|
||||
};
|
||||
@ -0,0 +1,54 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/DeveloperSettings.h"
|
||||
#include "PS_AI_Behavior_Settings.generated.h"
|
||||
|
||||
/**
|
||||
* Project-wide settings for the PS AI Behavior plugin.
|
||||
* Accessible via Project Settings -> Plugins -> PS AI Behavior.
|
||||
*/
|
||||
UCLASS(config = Game, defaultconfig, meta = (DisplayName = "PS AI Behavior"))
|
||||
class PS_AI_BEHAVIOR_API UPS_AI_Behavior_Settings : public UDeveloperSettings
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPS_AI_Behavior_Settings();
|
||||
|
||||
// ─── General ────────────────────────────────────────────────────────
|
||||
|
||||
/** Enable verbose logging for the behavior plugin. */
|
||||
UPROPERTY(config, EditAnywhere, Category = "General")
|
||||
bool bVerboseLogging = false;
|
||||
|
||||
// ─── Perception Defaults ────────────────────────────────────────────
|
||||
|
||||
/** Default sight radius for NPC perception (cm). */
|
||||
UPROPERTY(config, EditAnywhere, Category = "Perception", meta = (ClampMin = 100.0, ClampMax = 10000.0))
|
||||
float DefaultSightRadius = 6000.0f;
|
||||
|
||||
/** Default sight half-angle (degrees). */
|
||||
UPROPERTY(config, EditAnywhere, Category = "Perception", meta = (ClampMin = 10.0, ClampMax = 180.0))
|
||||
float DefaultSightHalfAngle = 45.0f;
|
||||
|
||||
/** Default hearing range (cm). */
|
||||
UPROPERTY(config, EditAnywhere, Category = "Perception", meta = (ClampMin = 100.0, ClampMax = 10000.0))
|
||||
float DefaultHearingRange = 3000.0f;
|
||||
|
||||
/** Seconds before a perceived stimulus is forgotten. */
|
||||
UPROPERTY(config, EditAnywhere, Category = "Perception", meta = (ClampMin = 1.0, ClampMax = 60.0))
|
||||
float PerceptionMaxAge = 10.0f;
|
||||
|
||||
// ─── Threat ─────────────────────────────────────────────────────────
|
||||
|
||||
/** Threat level decay rate per second when no threat is visible. */
|
||||
UPROPERTY(config, EditAnywhere, Category = "Threat", meta = (ClampMin = 0.0, ClampMax = 2.0))
|
||||
float ThreatDecayRate = 0.15f;
|
||||
|
||||
// ─── Section Name ───────────────────────────────────────────────────
|
||||
|
||||
virtual FName GetCategoryName() const override { return TEXT("Plugins"); }
|
||||
};
|
||||
@ -0,0 +1,169 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "PS_AI_Behavior_Definitions.h"
|
||||
#include "PS_AI_Behavior_SplineFollowerComponent.generated.h"
|
||||
|
||||
class APS_AI_Behavior_SplinePath;
|
||||
struct FPS_AI_Behavior_SplineJunction;
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnApproachingJunction,
|
||||
APS_AI_Behavior_SplinePath*, CurrentSpline,
|
||||
int32, JunctionIndex,
|
||||
float, DistanceToJunction);
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnSplineChanged,
|
||||
APS_AI_Behavior_SplinePath*, OldSpline,
|
||||
APS_AI_Behavior_SplinePath*, NewSpline);
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnSplineEndReached,
|
||||
APS_AI_Behavior_SplinePath*, Spline);
|
||||
|
||||
/**
|
||||
* Drives smooth NPC movement along spline paths.
|
||||
* Handles:
|
||||
* - Fluid motion with rotation interpolation
|
||||
* - Automatic junction detection and spline switching
|
||||
* - Speed variation based on spline settings
|
||||
* - Forward/reverse travel on bidirectional splines
|
||||
*
|
||||
* Attach to the NPC Pawn. Works with or without the AI Controller.
|
||||
*/
|
||||
UCLASS(ClassGroup = "PS AI Behavior", meta = (BlueprintSpawnableComponent, DisplayName = "PS AI Behavior - Spline Follower"))
|
||||
class PS_AI_BEHAVIOR_API UPS_AI_Behavior_SplineFollowerComponent : public UActorComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPS_AI_Behavior_SplineFollowerComponent();
|
||||
|
||||
// ─── Replication ────────────────────────────────────────────────────
|
||||
|
||||
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
|
||||
|
||||
// ─── Configuration ──────────────────────────────────────────────────
|
||||
|
||||
/** Walk speed along spline (cm/s). If the spline has its own speed, this is overridden. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spline Follower", meta = (ClampMin = "10.0"))
|
||||
float DefaultWalkSpeed = 150.0f;
|
||||
|
||||
/** How far ahead to look for junctions (cm). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spline Follower", meta = (ClampMin = "50.0"))
|
||||
float JunctionDetectionDistance = 300.0f;
|
||||
|
||||
/** How quickly the NPC rotates to face the spline direction (degrees/sec). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spline Follower", meta = (ClampMin = "30.0"))
|
||||
float RotationInterpSpeed = 360.0f;
|
||||
|
||||
/**
|
||||
* Whether to auto-choose a spline at junctions.
|
||||
* If false, OnApproachingJunction fires and you must call SwitchToSpline manually.
|
||||
* If true, uses SplineNetwork::ChooseSplineAtJunction automatically.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spline Follower")
|
||||
bool bAutoChooseAtJunction = true;
|
||||
|
||||
/**
|
||||
* If true, on reaching the end of a non-looped spline, reverse direction.
|
||||
* If false, stop and fire OnSplineEndReached.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spline Follower")
|
||||
bool bReverseAtEnd = false;
|
||||
|
||||
// ─── Runtime State ──────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Currently followed spline. Replicated so clients know which spline the NPC is on.
|
||||
* Null if not following any. Movement itself is synced via CMC.
|
||||
*/
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Replicated, Category = "Spline Follower|Runtime")
|
||||
TObjectPtr<APS_AI_Behavior_SplinePath> CurrentSpline;
|
||||
|
||||
/** Current distance along the spline (cm). Server-only, not replicated (CMC handles position). */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Spline Follower|Runtime")
|
||||
float CurrentDistance = 0.0f;
|
||||
|
||||
/** True if moving in the positive direction along the spline. */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Spline Follower|Runtime")
|
||||
bool bMovingForward = true;
|
||||
|
||||
/** Is the follower actively moving? Replicated for client animation state. */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Replicated, Category = "Spline Follower|Runtime")
|
||||
bool bIsFollowing = false;
|
||||
|
||||
// ─── Delegates ──────────────────────────────────────────────────────
|
||||
|
||||
/** Fired when approaching a junction. Use to make custom spline selection. */
|
||||
UPROPERTY(BlueprintAssignable, Category = "PS AI Behavior|Spline Follower")
|
||||
FOnApproachingJunction OnApproachingJunction;
|
||||
|
||||
/** Fired when the NPC switches to a different spline. */
|
||||
UPROPERTY(BlueprintAssignable, Category = "PS AI Behavior|Spline Follower")
|
||||
FOnSplineChanged OnSplineChanged;
|
||||
|
||||
/** Fired when the NPC reaches the end of a spline (if bReverseAtEnd is false). */
|
||||
UPROPERTY(BlueprintAssignable, Category = "PS AI Behavior|Spline Follower")
|
||||
FOnSplineEndReached OnSplineEndReached;
|
||||
|
||||
// ─── API ────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Start following the given spline from the closest point.
|
||||
* @param Spline The spline to follow.
|
||||
* @param bForward Direction of travel.
|
||||
* @return True if successfully started.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Spline Follower")
|
||||
bool StartFollowing(APS_AI_Behavior_SplinePath* Spline, bool bForward = true);
|
||||
|
||||
/**
|
||||
* Start following the given spline from a specific distance.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Spline Follower")
|
||||
bool StartFollowingAtDistance(APS_AI_Behavior_SplinePath* Spline, float StartDistance, bool bForward = true);
|
||||
|
||||
/** Stop following the current spline. */
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Spline Follower")
|
||||
void StopFollowing();
|
||||
|
||||
/** Pause/resume without losing state. */
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Spline Follower")
|
||||
void PauseFollowing();
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Spline Follower")
|
||||
void ResumeFollowing();
|
||||
|
||||
/**
|
||||
* Switch to another spline at a junction point.
|
||||
* @param NewSpline The spline to switch to.
|
||||
* @param DistanceOnNew Distance along the new spline to start from.
|
||||
* @param bNewForward Direction on the new spline.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Spline Follower")
|
||||
void SwitchToSpline(APS_AI_Behavior_SplinePath* NewSpline, float DistanceOnNew, bool bNewForward = true);
|
||||
|
||||
/** Get the effective walk speed (considering spline override). */
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Spline Follower")
|
||||
float GetEffectiveSpeed() const;
|
||||
|
||||
/** Get progress as a 0-1 ratio. */
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Spline Follower")
|
||||
float GetProgress() const;
|
||||
|
||||
protected:
|
||||
virtual void TickComponent(float DeltaTime, ELevelTick TickType,
|
||||
FActorComponentTickFunction* ThisTickFunction) override;
|
||||
|
||||
private:
|
||||
/** Check for upcoming junctions and handle them. */
|
||||
void HandleJunctions();
|
||||
|
||||
/** Index of the junction we already handled (to avoid re-triggering). */
|
||||
int32 LastHandledJunctionIndex = -1;
|
||||
|
||||
/** Speed multiplier for variety (set randomly on spawn). */
|
||||
float SpeedVariation = 1.0f;
|
||||
};
|
||||
@ -0,0 +1,108 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Subsystems/WorldSubsystem.h"
|
||||
#include "PS_AI_Behavior_Definitions.h"
|
||||
#include "PS_AI_Behavior_SplineNetwork.generated.h"
|
||||
|
||||
class APS_AI_Behavior_SplinePath;
|
||||
|
||||
/**
|
||||
* World Subsystem that manages the network of spline paths.
|
||||
* At BeginPlay, scans all SplinePath actors, detects intersections between them,
|
||||
* and populates their Junction arrays.
|
||||
*
|
||||
* Provides queries for NPCs to find the nearest accessible spline, pick a path
|
||||
* at a junction, etc.
|
||||
*/
|
||||
UCLASS()
|
||||
class PS_AI_BEHAVIOR_API UPS_AI_Behavior_SplineNetwork : public UWorldSubsystem
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
// ─── UWorldSubsystem ────────────────────────────────────────────────
|
||||
|
||||
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
|
||||
virtual void Deinitialize() override;
|
||||
|
||||
// ─── Network Build ──────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Scan the world for all SplinePath actors and compute junctions.
|
||||
* Called automatically after world initialization. Can be called again
|
||||
* if splines are added/removed at runtime.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|SplineNetwork")
|
||||
void RebuildNetwork();
|
||||
|
||||
// ─── Queries ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Find the closest accessible spline for the given NPC type.
|
||||
* @param WorldLocation The NPC's current position.
|
||||
* @param NPCType Filter: only return splines accessible to this type.
|
||||
* @param MaxDistance Maximum snap distance (cm). Default = 2000.
|
||||
* @param OutSpline The closest spline (if found).
|
||||
* @param OutDistance Distance along the spline to the closest point.
|
||||
* @return True if a spline was found within MaxDistance.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|SplineNetwork")
|
||||
bool FindClosestSpline(const FVector& WorldLocation,
|
||||
EPS_AI_Behavior_NPCType NPCType, float MaxDistance,
|
||||
APS_AI_Behavior_SplinePath*& OutSpline, float& OutDistance) const;
|
||||
|
||||
/**
|
||||
* Get all splines of a given category.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|SplineNetwork")
|
||||
TArray<APS_AI_Behavior_SplinePath*> GetSplinesForCategory(
|
||||
EPS_AI_Behavior_NPCType Category) const;
|
||||
|
||||
/**
|
||||
* Choose the best spline to switch to at a junction.
|
||||
* Considers spline priority, NPC personality (Caution → avoids main roads),
|
||||
* and optional bias away from a threat location.
|
||||
*
|
||||
* @param CurrentSpline The spline the NPC is currently on.
|
||||
* @param JunctionIndex Index into CurrentSpline->Junctions.
|
||||
* @param NPCType NPC type filter.
|
||||
* @param ThreatLocation Optional: bias away from this point. ZeroVector = ignore.
|
||||
* @param CautionScore Optional: NPC's caution trait (0-1). Higher = prefer quieter paths.
|
||||
* @return The chosen spline (could be the same if staying is best).
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|SplineNetwork")
|
||||
APS_AI_Behavior_SplinePath* ChooseSplineAtJunction(
|
||||
APS_AI_Behavior_SplinePath* CurrentSpline, int32 JunctionIndex,
|
||||
EPS_AI_Behavior_NPCType NPCType,
|
||||
const FVector& ThreatLocation = FVector::ZeroVector,
|
||||
float CautionScore = 0.5f) const;
|
||||
|
||||
/** Total number of splines in the network. */
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|SplineNetwork")
|
||||
int32 GetSplineCount() const { return AllSplines.Num(); }
|
||||
|
||||
/** Total number of junctions detected. */
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|SplineNetwork")
|
||||
int32 GetJunctionCount() const { return TotalJunctions; }
|
||||
|
||||
private:
|
||||
/** All registered spline paths. */
|
||||
UPROPERTY()
|
||||
TArray<TObjectPtr<APS_AI_Behavior_SplinePath>> AllSplines;
|
||||
|
||||
/** Cached junction count. */
|
||||
int32 TotalJunctions = 0;
|
||||
|
||||
/**
|
||||
* Detect junctions between two splines by sampling one and projecting onto the other.
|
||||
* Tolerance = max distance between splines to consider a junction.
|
||||
*/
|
||||
void DetectJunctions(APS_AI_Behavior_SplinePath* SplineA,
|
||||
APS_AI_Behavior_SplinePath* SplineB, float Tolerance);
|
||||
|
||||
/** UWorldSubsystem override — called when world begins play. */
|
||||
virtual void OnWorldBeginPlay(UWorld& InWorld) override;
|
||||
};
|
||||
@ -0,0 +1,144 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "PS_AI_Behavior_Definitions.h"
|
||||
#include "PS_AI_Behavior_SplinePath.generated.h"
|
||||
|
||||
class USplineComponent;
|
||||
|
||||
/**
|
||||
* A junction (intersection) between two splines.
|
||||
* Stored by the SplineNetwork subsystem after scanning overlaps.
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct FPS_AI_Behavior_SplineJunction
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/** The other spline at this junction. */
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Spline")
|
||||
TWeakObjectPtr<class APS_AI_Behavior_SplinePath> OtherSpline;
|
||||
|
||||
/** Distance along THIS spline where the junction is. */
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Spline")
|
||||
float DistanceOnThisSpline = 0.0f;
|
||||
|
||||
/** Distance along the OTHER spline where the junction is. */
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Spline")
|
||||
float DistanceOnOtherSpline = 0.0f;
|
||||
|
||||
/** World location of the junction. */
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Spline")
|
||||
FVector WorldLocation = FVector::ZeroVector;
|
||||
};
|
||||
|
||||
/**
|
||||
* Spline path actor — place in the level to define NPC navigation paths.
|
||||
* Think of it as a sidewalk, patrol route, or corridor.
|
||||
*
|
||||
* - Set SplineCategory to Civilian, Enemy, Protector, or Any to control access.
|
||||
* - Splines can overlap/intersect. The SplineNetwork subsystem detects junctions
|
||||
* and lets NPCs switch between paths at those points.
|
||||
* - Supports bidirectional travel by default.
|
||||
*/
|
||||
UCLASS(BlueprintType, Blueprintable, meta = (DisplayName = "PS AI Spline Path"))
|
||||
class PS_AI_BEHAVIOR_API APS_AI_Behavior_SplinePath : public AActor
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
APS_AI_Behavior_SplinePath();
|
||||
|
||||
// ─── Components ─────────────────────────────────────────────────────
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Spline")
|
||||
TObjectPtr<USplineComponent> SplineComp;
|
||||
|
||||
// ─── Configuration ──────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Which NPC type is allowed on this spline.
|
||||
* Civilian = civilians + protectors, Enemy = enemies only,
|
||||
* Protector = protectors only, Any = all types.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spline")
|
||||
EPS_AI_Behavior_NPCType SplineCategory = EPS_AI_Behavior_NPCType::Any;
|
||||
|
||||
/** If true, NPCs can travel in both directions on this spline. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spline")
|
||||
bool bBidirectional = true;
|
||||
|
||||
/**
|
||||
* Base walk speed on this spline (cm/s). 0 = use NPC's default speed.
|
||||
* Useful for making NPCs walk slower on narrow sidewalks.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spline", meta = (ClampMin = "0.0"))
|
||||
float SplineWalkSpeed = 0.0f;
|
||||
|
||||
/**
|
||||
* Priority when multiple splines are available at a junction.
|
||||
* Higher = more likely to be chosen. 0 = default.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spline")
|
||||
int32 Priority = 0;
|
||||
|
||||
// ─── Junctions (populated at runtime by SplineNetwork) ──────────────
|
||||
|
||||
/** All junctions on this spline, sorted by distance along spline. */
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Spline|Junctions")
|
||||
TArray<FPS_AI_Behavior_SplineJunction> Junctions;
|
||||
|
||||
// ─── API ────────────────────────────────────────────────────────────
|
||||
|
||||
/** Can the given NPC type use this spline? */
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Spline")
|
||||
bool IsAccessibleTo(EPS_AI_Behavior_NPCType NPCType) const;
|
||||
|
||||
/**
|
||||
* Get the closest point on this spline to a world location.
|
||||
* @param WorldLocation The reference point.
|
||||
* @param OutDistance Distance along the spline to the closest point.
|
||||
* @param OutWorldPoint World location of the closest point on the spline.
|
||||
* @return Distance from WorldLocation to the closest spline point.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Spline")
|
||||
float GetClosestPointOnSpline(const FVector& WorldLocation,
|
||||
float& OutDistance, FVector& OutWorldPoint) const;
|
||||
|
||||
/**
|
||||
* Get all junctions within a distance range on this spline.
|
||||
* @param CurrentDistance Current distance along the spline.
|
||||
* @param LookAheadDist How far ahead to look for junctions.
|
||||
* @param bForward Travel direction (true = increasing distance).
|
||||
* @return Array of upcoming junctions.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Spline")
|
||||
TArray<FPS_AI_Behavior_SplineJunction> GetUpcomingJunctions(
|
||||
float CurrentDistance, float LookAheadDist, bool bForward) const;
|
||||
|
||||
/** Total spline length. */
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Spline")
|
||||
float GetSplineLength() const;
|
||||
|
||||
/** Get world location at a distance along the spline. */
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Spline")
|
||||
FVector GetWorldLocationAtDistance(float Distance) const;
|
||||
|
||||
/** Get world rotation at a distance along the spline. */
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Spline")
|
||||
FRotator GetWorldRotationAtDistance(float Distance) const;
|
||||
|
||||
#if WITH_EDITOR
|
||||
virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
private:
|
||||
/** Update spline color in editor based on category. */
|
||||
void UpdateSplineVisualization();
|
||||
};
|
||||
@ -0,0 +1,33 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
using UnrealBuildTool;
|
||||
|
||||
public class PS_AI_BehaviorEditor : ModuleRules
|
||||
{
|
||||
public PS_AI_BehaviorEditor(ReadOnlyTargetRules Target) : base(Target)
|
||||
{
|
||||
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||
|
||||
PublicDependencyModuleNames.AddRange(new string[]
|
||||
{
|
||||
"Core",
|
||||
"CoreUObject",
|
||||
"Engine",
|
||||
"InputCore",
|
||||
"UnrealEd",
|
||||
"PS_AI_Behavior",
|
||||
});
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(new string[]
|
||||
{
|
||||
"Slate",
|
||||
"SlateCore",
|
||||
"EditorStyle",
|
||||
"EditorFramework",
|
||||
"PropertyEditor",
|
||||
"LevelEditor",
|
||||
"EditorSubsystem",
|
||||
"ComponentVisualizers",
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,134 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#include "PS_AI_BehaviorEditor.h"
|
||||
#include "PS_AI_Behavior_SplineEdMode.h"
|
||||
#include "PS_AI_Behavior_SplineVisualizer.h"
|
||||
#include "SPS_AI_Behavior_SplinePanel.h"
|
||||
#include "PS_AI_Behavior_Definitions.h"
|
||||
#include "EditorModeRegistry.h"
|
||||
#include "UnrealEdGlobals.h"
|
||||
#include "Editor/UnrealEdEngine.h"
|
||||
#include "LevelEditor.h"
|
||||
#include "Components/SplineComponent.h"
|
||||
#include "Widgets/Docking/SDockTab.h"
|
||||
#include "Framework/Docking/TabManager.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "FPS_AI_BehaviorEditorModule"
|
||||
|
||||
IMPLEMENT_MODULE(FPS_AI_BehaviorEditorModule, PS_AI_BehaviorEditor)
|
||||
|
||||
void FPS_AI_BehaviorEditorModule::StartupModule()
|
||||
{
|
||||
UE_LOG(LogPS_AI_Behavior, Log, TEXT("PS_AI_BehaviorEditor module started."));
|
||||
|
||||
RegisterEdMode();
|
||||
RegisterVisualizer();
|
||||
RegisterSplinePanel();
|
||||
}
|
||||
|
||||
void FPS_AI_BehaviorEditorModule::ShutdownModule()
|
||||
{
|
||||
UnregisterSplinePanel();
|
||||
UnregisterVisualizer();
|
||||
UnregisterEdMode();
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Log, TEXT("PS_AI_BehaviorEditor module shut down."));
|
||||
}
|
||||
|
||||
// ─── EdMode Registration ────────────────────────────────────────────────────
|
||||
|
||||
void FPS_AI_BehaviorEditorModule::RegisterEdMode()
|
||||
{
|
||||
FEditorModeRegistry::Get().RegisterMode<FPS_AI_Behavior_SplineEdMode>(
|
||||
FPS_AI_Behavior_SplineEdMode::EM_SplineEdModeId,
|
||||
LOCTEXT("SplineEdModeName", "PS AI Spline"),
|
||||
FSlateIcon(), // TODO: custom icon
|
||||
true // Visible in toolbar
|
||||
);
|
||||
}
|
||||
|
||||
void FPS_AI_BehaviorEditorModule::UnregisterEdMode()
|
||||
{
|
||||
FEditorModeRegistry::Get().UnregisterMode(FPS_AI_Behavior_SplineEdMode::EM_SplineEdModeId);
|
||||
}
|
||||
|
||||
// ─── Component Visualizer Registration ──────────────────────────────────────
|
||||
|
||||
void FPS_AI_BehaviorEditorModule::RegisterVisualizer()
|
||||
{
|
||||
if (GUnrealEd)
|
||||
{
|
||||
TSharedPtr<FPS_AI_Behavior_SplineVisualizer> Visualizer = MakeShareable(new FPS_AI_Behavior_SplineVisualizer);
|
||||
GUnrealEd->RegisterComponentVisualizer(USplineComponent::StaticClass()->GetFName(), Visualizer);
|
||||
|
||||
// Note: This registers for ALL USplineComponents. The visualizer checks
|
||||
// if the owner is a SplinePath before drawing anything extra.
|
||||
}
|
||||
}
|
||||
|
||||
void FPS_AI_BehaviorEditorModule::UnregisterVisualizer()
|
||||
{
|
||||
if (GUnrealEd)
|
||||
{
|
||||
GUnrealEd->UnregisterComponentVisualizer(USplineComponent::StaticClass()->GetFName());
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Detail Customizations ──────────────────────────────────────────────────
|
||||
|
||||
void FPS_AI_BehaviorEditorModule::RegisterDetailCustomizations()
|
||||
{
|
||||
// TODO: Register detail customization for APS_AI_Behavior_SplinePath
|
||||
}
|
||||
|
||||
void FPS_AI_BehaviorEditorModule::UnregisterDetailCustomizations()
|
||||
{
|
||||
// TODO: Unregister detail customizations
|
||||
}
|
||||
|
||||
// ─── Spline Panel Tab ───────────────────────────────────────────────────────
|
||||
|
||||
void FPS_AI_BehaviorEditorModule::RegisterSplinePanel()
|
||||
{
|
||||
FGlobalTabmanager::Get()->RegisterNomadTabSpawner(
|
||||
SPS_AI_Behavior_SplinePanel::TabId,
|
||||
FOnSpawnTab::CreateLambda([](const FSpawnTabArgs& Args) -> TSharedRef<SDockTab>
|
||||
{
|
||||
return SNew(SDockTab)
|
||||
.TabRole(ETabRole::NomadTab)
|
||||
.Label(LOCTEXT("SplinePanelTabLabel", "PS AI Spline Network"))
|
||||
[
|
||||
SNew(SPS_AI_Behavior_SplinePanel)
|
||||
];
|
||||
}))
|
||||
.SetDisplayName(LOCTEXT("SplinePanelDisplayName", "PS AI Spline Network"))
|
||||
.SetMenuType(ETabSpawnerMenuType::Hidden);
|
||||
|
||||
// Add to Window menu via Level Editor
|
||||
FLevelEditorModule& LevelEditor = FModuleManager::LoadModuleChecked<FLevelEditorModule>("LevelEditor");
|
||||
TSharedPtr<FExtender> MenuExtender = MakeShareable(new FExtender);
|
||||
MenuExtender->AddMenuExtension(
|
||||
"WindowLayout",
|
||||
EExtensionHook::After,
|
||||
nullptr,
|
||||
FMenuExtensionDelegate::CreateLambda([](FMenuBuilder& MenuBuilder)
|
||||
{
|
||||
MenuBuilder.AddMenuEntry(
|
||||
LOCTEXT("SplinePanelMenuEntry", "PS AI Spline Network"),
|
||||
LOCTEXT("SplinePanelMenuTooltip", "Open the PS AI Spline Network management panel"),
|
||||
FSlateIcon(),
|
||||
FUIAction(FExecuteAction::CreateLambda([]()
|
||||
{
|
||||
FGlobalTabmanager::Get()->TryInvokeTab(SPS_AI_Behavior_SplinePanel::TabId);
|
||||
}))
|
||||
);
|
||||
}));
|
||||
LevelEditor.GetMenuExtensibilityManager()->AddExtender(MenuExtender);
|
||||
}
|
||||
|
||||
void FPS_AI_BehaviorEditorModule::UnregisterSplinePanel()
|
||||
{
|
||||
FGlobalTabmanager::Get()->UnregisterNomadTabSpawner(SPS_AI_Behavior_SplinePanel::TabId);
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
@ -0,0 +1,433 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#include "PS_AI_Behavior_SplineEdMode.h"
|
||||
#include "PS_AI_Behavior_SplineEdModeToolkit.h"
|
||||
#include "PS_AI_Behavior_SplinePath.h"
|
||||
#include "PS_AI_Behavior_SplineNetwork.h"
|
||||
#include "PS_AI_Behavior_CoverPoint.h"
|
||||
#include "Components/SplineComponent.h"
|
||||
#include "EditorModeManager.h"
|
||||
#include "Engine/World.h"
|
||||
#include "EngineUtils.h"
|
||||
#include "Editor.h"
|
||||
#include "Toolkits/ToolkitManager.h"
|
||||
#include "DrawDebugHelpers.h"
|
||||
#include "CollisionQueryParams.h"
|
||||
|
||||
const FEditorModeID FPS_AI_Behavior_SplineEdMode::EM_SplineEdModeId = TEXT("EM_PS_AI_BehaviorSpline");
|
||||
|
||||
FPS_AI_Behavior_SplineEdMode::FPS_AI_Behavior_SplineEdMode()
|
||||
{
|
||||
}
|
||||
|
||||
FPS_AI_Behavior_SplineEdMode::~FPS_AI_Behavior_SplineEdMode()
|
||||
{
|
||||
}
|
||||
|
||||
void FPS_AI_Behavior_SplineEdMode::Enter()
|
||||
{
|
||||
FEdMode::Enter();
|
||||
|
||||
// Create toolkit (toolbar widget)
|
||||
if (!Toolkit.IsValid())
|
||||
{
|
||||
Toolkit = MakeShareable(new FPS_AI_Behavior_SplineEdModeToolkit);
|
||||
Toolkit->Init(Owner->GetToolkitHost());
|
||||
}
|
||||
}
|
||||
|
||||
void FPS_AI_Behavior_SplineEdMode::Exit()
|
||||
{
|
||||
// Finalize any in-progress spline
|
||||
if (ActiveSpline)
|
||||
{
|
||||
FinalizeCurrentSpline();
|
||||
}
|
||||
|
||||
if (Toolkit.IsValid())
|
||||
{
|
||||
FToolkitManager::Get().CloseToolkit(Toolkit.ToSharedRef());
|
||||
Toolkit.Reset();
|
||||
}
|
||||
|
||||
FEdMode::Exit();
|
||||
}
|
||||
|
||||
bool FPS_AI_Behavior_SplineEdMode::HandleClick(
|
||||
FEditorViewportClient* InViewportClient, HHitProxy* HitProxy, const FViewportClick& Click)
|
||||
{
|
||||
if (Click.GetKey() != EKeys::LeftMouseButton)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
UWorld* World = GetWorld();
|
||||
if (!World) return false;
|
||||
|
||||
// Get click location via line trace from camera
|
||||
FViewport* Viewport = InViewportClient->Viewport;
|
||||
if (!Viewport) return false;
|
||||
|
||||
const int32 HitX = Viewport->GetMouseX();
|
||||
const int32 HitY = Viewport->GetMouseY();
|
||||
|
||||
FSceneViewFamilyContext ViewFamily(FSceneViewFamily::ConstructionValues(
|
||||
Viewport, InViewportClient->GetScene(),
|
||||
InViewportClient->EngineShowFlags));
|
||||
FSceneView* View = InViewportClient->CalcSceneView(&ViewFamily);
|
||||
|
||||
// Deproject mouse to world
|
||||
FVector2D MousePos(HitX, HitY);
|
||||
FVector RayOrigin, RayDirection;
|
||||
FSceneView::DeprojectScreenToWorld(
|
||||
MousePos, View->UnconstrainedViewRect,
|
||||
View->ViewMatrices.GetInvViewProjectionMatrix(),
|
||||
RayOrigin, RayDirection);
|
||||
|
||||
// Line trace to find ground
|
||||
FHitResult Hit;
|
||||
FCollisionQueryParams Params(SCENE_QUERY_STAT(SplineEdModeClick), true);
|
||||
const FVector TraceEnd = RayOrigin + RayDirection * 100000.0f;
|
||||
|
||||
if (!World->LineTraceSingleByChannel(Hit, RayOrigin, TraceEnd, ECC_WorldStatic, Params))
|
||||
{
|
||||
return false; // No ground hit
|
||||
}
|
||||
|
||||
FVector ClickLocation = Hit.ImpactPoint;
|
||||
|
||||
// Ctrl+Click on existing spline → select for extension
|
||||
if (Click.IsControlDown())
|
||||
{
|
||||
// Check if we hit a SplinePath
|
||||
AActor* HitActor = Hit.GetActor();
|
||||
APS_AI_Behavior_SplinePath* HitSpline = Cast<APS_AI_Behavior_SplinePath>(HitActor);
|
||||
|
||||
if (!HitSpline)
|
||||
{
|
||||
// Check nearby splines
|
||||
for (TActorIterator<APS_AI_Behavior_SplinePath> It(World); It; ++It)
|
||||
{
|
||||
float Dist = 0.0f;
|
||||
FVector ClosestPt;
|
||||
if ((*It)->GetClosestPointOnSpline(ClickLocation, Dist, ClosestPt) < 200.0f)
|
||||
{
|
||||
HitSpline = *It;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (HitSpline)
|
||||
{
|
||||
SelectSplineForExtension(HitSpline);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Snap to ground
|
||||
if (bSnapToGround)
|
||||
{
|
||||
SnapToGround(ClickLocation);
|
||||
}
|
||||
|
||||
// ─── Route to active tool ───────────────────────────────────────────
|
||||
switch (ActiveTool)
|
||||
{
|
||||
case EPS_AI_Behavior_EdModeTool::Spline:
|
||||
AddPointToSpline(ClickLocation);
|
||||
break;
|
||||
|
||||
case EPS_AI_Behavior_EdModeTool::CoverPoint:
|
||||
{
|
||||
// Cover point faces toward the camera (typical workflow)
|
||||
const FVector CamLoc = InViewportClient->GetViewLocation();
|
||||
const FVector DirToCamera = (CamLoc - ClickLocation).GetSafeNormal2D();
|
||||
const FRotator Facing = DirToCamera.Rotation();
|
||||
|
||||
APS_AI_Behavior_CoverPoint* NewPoint = PlaceCoverPoint(ClickLocation, Facing);
|
||||
if (NewPoint)
|
||||
{
|
||||
GEditor->SelectNone(true, true);
|
||||
GEditor->SelectActor(NewPoint, true, true);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FPS_AI_Behavior_SplineEdMode::InputKey(
|
||||
FEditorViewportClient* ViewportClient, FViewport* Viewport,
|
||||
FKey Key, EInputEvent Event)
|
||||
{
|
||||
if (Event != IE_Pressed)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Enter → finalize current spline
|
||||
if (Key == EKeys::Enter || Key == EKeys::SpaceBar)
|
||||
{
|
||||
if (ActiveSpline && PointCount >= 2)
|
||||
{
|
||||
FinalizeCurrentSpline();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Escape → cancel current spline or exit mode
|
||||
if (Key == EKeys::Escape)
|
||||
{
|
||||
if (ActiveSpline)
|
||||
{
|
||||
// Delete the in-progress spline
|
||||
ActiveSpline->Destroy();
|
||||
ActiveSpline = nullptr;
|
||||
PointCount = 0;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete → delete selected spline
|
||||
if (Key == EKeys::Delete)
|
||||
{
|
||||
if (ActiveSpline)
|
||||
{
|
||||
ActiveSpline->Destroy();
|
||||
ActiveSpline = nullptr;
|
||||
PointCount = 0;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void FPS_AI_Behavior_SplineEdMode::Render(
|
||||
const FSceneView* View, FViewport* Viewport, FPrimitiveDrawInterface* PDI)
|
||||
{
|
||||
FEdMode::Render(View, Viewport, PDI);
|
||||
|
||||
if (!bShowJunctionPreview) return;
|
||||
|
||||
UWorld* World = GetWorld();
|
||||
if (!World) return;
|
||||
|
||||
// Draw junction spheres for all splines in the level
|
||||
for (TActorIterator<APS_AI_Behavior_SplinePath> It(World); It; ++It)
|
||||
{
|
||||
for (const FPS_AI_Behavior_SplineJunction& J : (*It)->Junctions)
|
||||
{
|
||||
// Yellow sphere at junction
|
||||
PDI->DrawPoint(J.WorldLocation, FLinearColor::Yellow, 12.0f, SDPG_Foreground);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw arrow heads on splines to show direction
|
||||
for (TActorIterator<APS_AI_Behavior_SplinePath> It(World); It; ++It)
|
||||
{
|
||||
APS_AI_Behavior_SplinePath* Spline = *It;
|
||||
if (!Spline->SplineComp) continue;
|
||||
|
||||
const float Length = Spline->GetSplineLength();
|
||||
if (Length <= 0.0f) continue;
|
||||
|
||||
// Draw arrows every 500cm
|
||||
const float ArrowSpacing = 500.0f;
|
||||
for (float Dist = ArrowSpacing; Dist < Length; Dist += ArrowSpacing)
|
||||
{
|
||||
const FVector Pos = Spline->GetWorldLocationAtDistance(Dist);
|
||||
const FVector Dir = Spline->SplineComp->GetDirectionAtDistanceAlongSpline(
|
||||
Dist, ESplineCoordinateSpace::World);
|
||||
|
||||
// Draw direction line
|
||||
const FVector ArrowEnd = Pos + Dir * 60.0f;
|
||||
PDI->DrawLine(Pos, ArrowEnd, FLinearColor::White, SDPG_Foreground, 2.0f);
|
||||
|
||||
// Arrow head
|
||||
const FVector Right = FVector::CrossProduct(Dir, FVector::UpVector).GetSafeNormal();
|
||||
PDI->DrawLine(ArrowEnd, ArrowEnd - Dir * 25.0f + Right * 15.0f,
|
||||
FLinearColor::White, SDPG_Foreground, 2.0f);
|
||||
PDI->DrawLine(ArrowEnd, ArrowEnd - Dir * 25.0f - Right * 15.0f,
|
||||
FLinearColor::White, SDPG_Foreground, 2.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool FPS_AI_Behavior_SplineEdMode::IsCompatibleWith(FEditorModeID OtherModeID) const
|
||||
{
|
||||
return true; // Compatible with all other modes
|
||||
}
|
||||
|
||||
// ─── Spline Building ────────────────────────────────────────────────────────
|
||||
|
||||
APS_AI_Behavior_SplinePath* FPS_AI_Behavior_SplineEdMode::SpawnNewSpline(const FVector& FirstPoint)
|
||||
{
|
||||
UWorld* World = GetWorld();
|
||||
if (!World) return nullptr;
|
||||
|
||||
FActorSpawnParameters SpawnParams;
|
||||
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
||||
|
||||
APS_AI_Behavior_SplinePath* NewSpline = World->SpawnActor<APS_AI_Behavior_SplinePath>(
|
||||
APS_AI_Behavior_SplinePath::StaticClass(), FTransform(FirstPoint), SpawnParams);
|
||||
|
||||
if (NewSpline)
|
||||
{
|
||||
NewSpline->SplineCategory = CurrentSplineType;
|
||||
|
||||
// Clear default spline points and set first point
|
||||
NewSpline->SplineComp->ClearSplinePoints(false);
|
||||
NewSpline->SplineComp->AddSplineWorldPoint(FirstPoint);
|
||||
NewSpline->SplineComp->UpdateSpline();
|
||||
|
||||
// Label in outliner
|
||||
const FString TypeName = UEnum::GetDisplayValueAsText(CurrentSplineType).ToString();
|
||||
NewSpline->SetActorLabel(FString::Printf(TEXT("SplinePath_%s"), *TypeName));
|
||||
|
||||
// Register with undo
|
||||
GEditor->BeginTransaction(FText::FromString(TEXT("Create Spline Path")));
|
||||
NewSpline->Modify();
|
||||
GEditor->EndTransaction();
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Log, TEXT("EdMode: Created new %s spline at (%.0f, %.0f, %.0f)"),
|
||||
*TypeName, FirstPoint.X, FirstPoint.Y, FirstPoint.Z);
|
||||
}
|
||||
|
||||
return NewSpline;
|
||||
}
|
||||
|
||||
void FPS_AI_Behavior_SplineEdMode::AddPointToSpline(const FVector& WorldLocation)
|
||||
{
|
||||
if (!ActiveSpline)
|
||||
{
|
||||
// First click — spawn new spline
|
||||
ActiveSpline = SpawnNewSpline(WorldLocation);
|
||||
PointCount = ActiveSpline ? 1 : 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// Add point to existing spline
|
||||
GEditor->BeginTransaction(FText::FromString(TEXT("Add Spline Point")));
|
||||
ActiveSpline->Modify();
|
||||
|
||||
ActiveSpline->SplineComp->AddSplineWorldPoint(WorldLocation);
|
||||
ActiveSpline->SplineComp->UpdateSpline();
|
||||
++PointCount;
|
||||
|
||||
GEditor->EndTransaction();
|
||||
}
|
||||
|
||||
void FPS_AI_Behavior_SplineEdMode::FinalizeCurrentSpline()
|
||||
{
|
||||
if (!ActiveSpline) return;
|
||||
|
||||
if (PointCount < 2)
|
||||
{
|
||||
// Not enough points — delete
|
||||
ActiveSpline->Destroy();
|
||||
ActiveSpline = nullptr;
|
||||
PointCount = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Log, TEXT("EdMode: Finalized spline '%s' with %d points, length %.0fcm"),
|
||||
*ActiveSpline->GetActorLabel(), PointCount, ActiveSpline->GetSplineLength());
|
||||
|
||||
// Rebuild network to detect new junctions
|
||||
RebuildNetworkPreview();
|
||||
|
||||
ActiveSpline = nullptr;
|
||||
PointCount = 0;
|
||||
}
|
||||
|
||||
void FPS_AI_Behavior_SplineEdMode::SelectSplineForExtension(APS_AI_Behavior_SplinePath* Spline)
|
||||
{
|
||||
if (!Spline) return;
|
||||
|
||||
// Finalize any current spline first
|
||||
if (ActiveSpline && ActiveSpline != Spline)
|
||||
{
|
||||
FinalizeCurrentSpline();
|
||||
}
|
||||
|
||||
ActiveSpline = Spline;
|
||||
PointCount = Spline->SplineComp ? Spline->SplineComp->GetNumberOfSplinePoints() : 0;
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Log, TEXT("EdMode: Selected spline '%s' for extension (%d existing points)"),
|
||||
*Spline->GetActorLabel(), PointCount);
|
||||
}
|
||||
|
||||
// ─── Cover Point Placement ──────────────────────────────────────────────────
|
||||
|
||||
APS_AI_Behavior_CoverPoint* FPS_AI_Behavior_SplineEdMode::PlaceCoverPoint(
|
||||
const FVector& WorldLocation, const FRotator& Facing)
|
||||
{
|
||||
UWorld* World = GetWorld();
|
||||
if (!World) return nullptr;
|
||||
|
||||
GEditor->BeginTransaction(FText::FromString(TEXT("Place Cover Point")));
|
||||
|
||||
FActorSpawnParameters SpawnParams;
|
||||
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
||||
|
||||
APS_AI_Behavior_CoverPoint* NewPoint = World->SpawnActor<APS_AI_Behavior_CoverPoint>(
|
||||
APS_AI_Behavior_CoverPoint::StaticClass(),
|
||||
FTransform(Facing, WorldLocation),
|
||||
SpawnParams);
|
||||
|
||||
if (NewPoint)
|
||||
{
|
||||
NewPoint->PointType = CurrentCoverType;
|
||||
NewPoint->AllowedNPCType = CoverAllowedNPCType;
|
||||
NewPoint->Modify();
|
||||
|
||||
const FString TypeName = CurrentCoverType == EPS_AI_Behavior_CoverPointType::Cover
|
||||
? TEXT("Cover") : TEXT("HidingSpot");
|
||||
NewPoint->SetActorLabel(FString::Printf(TEXT("%s_%d"), *TypeName,
|
||||
FMath::RandRange(1000, 9999)));
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Log, TEXT("EdMode: Placed %s at (%.0f, %.0f, %.0f)"),
|
||||
*TypeName, WorldLocation.X, WorldLocation.Y, WorldLocation.Z);
|
||||
}
|
||||
|
||||
GEditor->EndTransaction();
|
||||
return NewPoint;
|
||||
}
|
||||
|
||||
bool FPS_AI_Behavior_SplineEdMode::SnapToGround(FVector& InOutLocation) const
|
||||
{
|
||||
UWorld* World = GetWorld();
|
||||
if (!World) return false;
|
||||
|
||||
// Trace downward from above the point
|
||||
const FVector TraceStart = InOutLocation + FVector(0, 0, 500.0f);
|
||||
const FVector TraceEnd = InOutLocation - FVector(0, 0, 5000.0f);
|
||||
|
||||
FHitResult Hit;
|
||||
FCollisionQueryParams Params(SCENE_QUERY_STAT(SplineSnapToGround), true);
|
||||
|
||||
if (World->LineTraceSingleByChannel(Hit, TraceStart, TraceEnd, ECC_WorldStatic, Params))
|
||||
{
|
||||
InOutLocation = Hit.ImpactPoint + FVector(0, 0, 5.0f); // Slight offset above ground
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void FPS_AI_Behavior_SplineEdMode::RebuildNetworkPreview()
|
||||
{
|
||||
UWorld* World = GetWorld();
|
||||
if (!World) return;
|
||||
|
||||
UPS_AI_Behavior_SplineNetwork* Network = World->GetSubsystem<UPS_AI_Behavior_SplineNetwork>();
|
||||
if (Network)
|
||||
{
|
||||
Network->RebuildNetwork();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,329 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#include "PS_AI_Behavior_SplineEdModeToolkit.h"
|
||||
#include "PS_AI_Behavior_SplineEdMode.h"
|
||||
#include "EditorModeManager.h"
|
||||
#include "Widgets/Input/SButton.h"
|
||||
#include "Widgets/Input/SCheckBox.h"
|
||||
#include "Widgets/Layout/SBorder.h"
|
||||
#include "Widgets/Layout/SBox.h"
|
||||
#include "Widgets/Text/STextBlock.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "PS_AI_BehaviorSplineToolkit"
|
||||
|
||||
void FPS_AI_Behavior_SplineEdModeToolkit::Init(const TSharedPtr<IToolkitHost>& InitToolkitHost)
|
||||
{
|
||||
ToolkitWidget = BuildToolkitWidget();
|
||||
FModeToolkit::Init(InitToolkitHost);
|
||||
}
|
||||
|
||||
FEdMode* FPS_AI_Behavior_SplineEdModeToolkit::GetEditorMode() const
|
||||
{
|
||||
return GLevelEditorModeTools().GetActiveMode(FPS_AI_Behavior_SplineEdMode::EM_SplineEdModeId);
|
||||
}
|
||||
|
||||
FPS_AI_Behavior_SplineEdMode* FPS_AI_Behavior_SplineEdModeToolkit::GetSplineEdMode() const
|
||||
{
|
||||
return static_cast<FPS_AI_Behavior_SplineEdMode*>(GetEditorMode());
|
||||
}
|
||||
|
||||
TSharedRef<SWidget> FPS_AI_Behavior_SplineEdModeToolkit::BuildToolkitWidget()
|
||||
{
|
||||
return SNew(SBorder)
|
||||
.Padding(8.0f)
|
||||
[
|
||||
SNew(SVerticalBox)
|
||||
|
||||
// ─── Title ──────────────────────────────────────────────
|
||||
+ SVerticalBox::Slot()
|
||||
.AutoHeight()
|
||||
.Padding(0, 0, 0, 8)
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(LOCTEXT("Title", "PS AI Level Design"))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 14))
|
||||
]
|
||||
|
||||
// ─── Tool Selection ─────────────────────────────────────
|
||||
+ SVerticalBox::Slot()
|
||||
.AutoHeight()
|
||||
.Padding(0, 0, 0, 4)
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(LOCTEXT("ToolLabel", "Active Tool:"))
|
||||
]
|
||||
|
||||
+ SVerticalBox::Slot()
|
||||
.AutoHeight()
|
||||
.Padding(0, 0, 0, 8)
|
||||
[
|
||||
SNew(SHorizontalBox)
|
||||
|
||||
+ SHorizontalBox::Slot()
|
||||
.AutoWidth()
|
||||
.Padding(0, 0, 4, 0)
|
||||
[
|
||||
SNew(SButton)
|
||||
.Text(LOCTEXT("ToolSpline", "Spline"))
|
||||
.ButtonColorAndOpacity_Lambda([this]()
|
||||
{
|
||||
auto* Mode = GetSplineEdMode();
|
||||
const bool bActive = Mode && Mode->ActiveTool == EPS_AI_Behavior_EdModeTool::Spline;
|
||||
return FSlateColor(bActive ? FLinearColor::White : FLinearColor(0.3f, 0.3f, 0.3f));
|
||||
})
|
||||
.OnClicked_Lambda([this]()
|
||||
{
|
||||
if (auto* Mode = GetSplineEdMode()) Mode->ActiveTool = EPS_AI_Behavior_EdModeTool::Spline;
|
||||
return FReply::Handled();
|
||||
})
|
||||
]
|
||||
|
||||
+ SHorizontalBox::Slot()
|
||||
.AutoWidth()
|
||||
[
|
||||
SNew(SButton)
|
||||
.Text(LOCTEXT("ToolCover", "Cover Point"))
|
||||
.ButtonColorAndOpacity_Lambda([this]()
|
||||
{
|
||||
auto* Mode = GetSplineEdMode();
|
||||
const bool bActive = Mode && Mode->ActiveTool == EPS_AI_Behavior_EdModeTool::CoverPoint;
|
||||
return FSlateColor(bActive ? FLinearColor::White : FLinearColor(0.3f, 0.3f, 0.3f));
|
||||
})
|
||||
.OnClicked_Lambda([this]()
|
||||
{
|
||||
if (auto* Mode = GetSplineEdMode()) Mode->ActiveTool = EPS_AI_Behavior_EdModeTool::CoverPoint;
|
||||
return FReply::Handled();
|
||||
})
|
||||
]
|
||||
]
|
||||
|
||||
// ─── Spline Type Selection ──────────────────────────────
|
||||
+ SVerticalBox::Slot()
|
||||
.AutoHeight()
|
||||
.Padding(0, 0, 0, 4)
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(LOCTEXT("TypeLabel", "Spline Type:"))
|
||||
]
|
||||
|
||||
+ SVerticalBox::Slot()
|
||||
.AutoHeight()
|
||||
.Padding(0, 0, 0, 8)
|
||||
[
|
||||
SNew(SHorizontalBox)
|
||||
|
||||
+ SHorizontalBox::Slot()
|
||||
.AutoWidth()
|
||||
.Padding(0, 0, 4, 0)
|
||||
[
|
||||
SNew(SButton)
|
||||
.Text(LOCTEXT("Civilian", "Civilian"))
|
||||
.ButtonColorAndOpacity(this, &FPS_AI_Behavior_SplineEdModeToolkit::GetTypeColor, EPS_AI_Behavior_NPCType::Civilian)
|
||||
.OnClicked(this, &FPS_AI_Behavior_SplineEdModeToolkit::OnTypeButtonClicked, EPS_AI_Behavior_NPCType::Civilian)
|
||||
]
|
||||
|
||||
+ SHorizontalBox::Slot()
|
||||
.AutoWidth()
|
||||
.Padding(0, 0, 4, 0)
|
||||
[
|
||||
SNew(SButton)
|
||||
.Text(LOCTEXT("Enemy", "Enemy"))
|
||||
.ButtonColorAndOpacity(this, &FPS_AI_Behavior_SplineEdModeToolkit::GetTypeColor, EPS_AI_Behavior_NPCType::Enemy)
|
||||
.OnClicked(this, &FPS_AI_Behavior_SplineEdModeToolkit::OnTypeButtonClicked, EPS_AI_Behavior_NPCType::Enemy)
|
||||
]
|
||||
|
||||
+ SHorizontalBox::Slot()
|
||||
.AutoWidth()
|
||||
.Padding(0, 0, 4, 0)
|
||||
[
|
||||
SNew(SButton)
|
||||
.Text(LOCTEXT("Protector", "Protector"))
|
||||
.ButtonColorAndOpacity(this, &FPS_AI_Behavior_SplineEdModeToolkit::GetTypeColor, EPS_AI_Behavior_NPCType::Protector)
|
||||
.OnClicked(this, &FPS_AI_Behavior_SplineEdModeToolkit::OnTypeButtonClicked, EPS_AI_Behavior_NPCType::Protector)
|
||||
]
|
||||
|
||||
+ SHorizontalBox::Slot()
|
||||
.AutoWidth()
|
||||
[
|
||||
SNew(SButton)
|
||||
.Text(LOCTEXT("Any", "Any"))
|
||||
.ButtonColorAndOpacity(this, &FPS_AI_Behavior_SplineEdModeToolkit::GetTypeColor, EPS_AI_Behavior_NPCType::Any)
|
||||
.OnClicked(this, &FPS_AI_Behavior_SplineEdModeToolkit::OnTypeButtonClicked, EPS_AI_Behavior_NPCType::Any)
|
||||
]
|
||||
]
|
||||
|
||||
// ─── Options ────────────────────────────────────────────
|
||||
+ SVerticalBox::Slot()
|
||||
.AutoHeight()
|
||||
.Padding(0, 0, 0, 4)
|
||||
[
|
||||
SNew(SHorizontalBox)
|
||||
|
||||
+ SHorizontalBox::Slot()
|
||||
.AutoWidth()
|
||||
.VAlign(VAlign_Center)
|
||||
.Padding(0, 0, 8, 0)
|
||||
[
|
||||
SNew(SCheckBox)
|
||||
.IsChecked_Lambda([this]()
|
||||
{
|
||||
auto* Mode = GetSplineEdMode();
|
||||
return Mode && Mode->bSnapToGround ? ECheckBoxState::Checked : ECheckBoxState::Unchecked;
|
||||
})
|
||||
.OnCheckStateChanged_Lambda([this](ECheckBoxState NewState)
|
||||
{
|
||||
if (auto* Mode = GetSplineEdMode())
|
||||
{
|
||||
Mode->bSnapToGround = (NewState == ECheckBoxState::Checked);
|
||||
}
|
||||
})
|
||||
]
|
||||
|
||||
+ SHorizontalBox::Slot()
|
||||
.AutoWidth()
|
||||
.VAlign(VAlign_Center)
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(LOCTEXT("SnapToGround", "Snap to Ground"))
|
||||
]
|
||||
]
|
||||
|
||||
+ SVerticalBox::Slot()
|
||||
.AutoHeight()
|
||||
.Padding(0, 0, 0, 8)
|
||||
[
|
||||
SNew(SHorizontalBox)
|
||||
|
||||
+ SHorizontalBox::Slot()
|
||||
.AutoWidth()
|
||||
.VAlign(VAlign_Center)
|
||||
.Padding(0, 0, 8, 0)
|
||||
[
|
||||
SNew(SCheckBox)
|
||||
.IsChecked_Lambda([this]()
|
||||
{
|
||||
auto* Mode = GetSplineEdMode();
|
||||
return Mode && Mode->bShowJunctionPreview ? ECheckBoxState::Checked : ECheckBoxState::Unchecked;
|
||||
})
|
||||
.OnCheckStateChanged_Lambda([this](ECheckBoxState NewState)
|
||||
{
|
||||
if (auto* Mode = GetSplineEdMode())
|
||||
{
|
||||
Mode->bShowJunctionPreview = (NewState == ECheckBoxState::Checked);
|
||||
}
|
||||
})
|
||||
]
|
||||
|
||||
+ SHorizontalBox::Slot()
|
||||
.AutoWidth()
|
||||
.VAlign(VAlign_Center)
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(LOCTEXT("ShowJunctions", "Show Junction Preview"))
|
||||
]
|
||||
]
|
||||
|
||||
// ─── Cover Point Type ───────────────────────────────────
|
||||
+ SVerticalBox::Slot()
|
||||
.AutoHeight()
|
||||
.Padding(0, 0, 0, 4)
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(LOCTEXT("CoverTypeLabel", "Cover Point Type:"))
|
||||
]
|
||||
|
||||
+ SVerticalBox::Slot()
|
||||
.AutoHeight()
|
||||
.Padding(0, 0, 0, 8)
|
||||
[
|
||||
SNew(SHorizontalBox)
|
||||
|
||||
+ SHorizontalBox::Slot()
|
||||
.AutoWidth()
|
||||
.Padding(0, 0, 4, 0)
|
||||
[
|
||||
SNew(SButton)
|
||||
.Text(LOCTEXT("CoverType", "Cover"))
|
||||
.ButtonColorAndOpacity_Lambda([this]()
|
||||
{
|
||||
auto* Mode = GetSplineEdMode();
|
||||
const bool bActive = Mode && Mode->CurrentCoverType == EPS_AI_Behavior_CoverPointType::Cover;
|
||||
return FSlateColor(bActive ? FLinearColor(0.2f, 0.5f, 1.0f) : FLinearColor(0.15f, 0.25f, 0.5f));
|
||||
})
|
||||
.OnClicked_Lambda([this]()
|
||||
{
|
||||
if (auto* Mode = GetSplineEdMode()) Mode->CurrentCoverType = EPS_AI_Behavior_CoverPointType::Cover;
|
||||
return FReply::Handled();
|
||||
})
|
||||
]
|
||||
|
||||
+ SHorizontalBox::Slot()
|
||||
.AutoWidth()
|
||||
[
|
||||
SNew(SButton)
|
||||
.Text(LOCTEXT("HidingType", "Hiding Spot"))
|
||||
.ButtonColorAndOpacity_Lambda([this]()
|
||||
{
|
||||
auto* Mode = GetSplineEdMode();
|
||||
const bool bActive = Mode && Mode->CurrentCoverType == EPS_AI_Behavior_CoverPointType::HidingSpot;
|
||||
return FSlateColor(bActive ? FLinearColor(1.0f, 0.85f, 0.0f) : FLinearColor(0.5f, 0.42f, 0.0f));
|
||||
})
|
||||
.OnClicked_Lambda([this]()
|
||||
{
|
||||
if (auto* Mode = GetSplineEdMode()) Mode->CurrentCoverType = EPS_AI_Behavior_CoverPointType::HidingSpot;
|
||||
return FReply::Handled();
|
||||
})
|
||||
]
|
||||
]
|
||||
|
||||
// ─── Instructions ───────────────────────────────────────
|
||||
+ SVerticalBox::Slot()
|
||||
.AutoHeight()
|
||||
.Padding(0, 8, 0, 0)
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(LOCTEXT("Instructions",
|
||||
"SPLINE TOOL:\n"
|
||||
" LMB: Add point\n"
|
||||
" Ctrl+LMB: Select to extend\n"
|
||||
" Enter/Space: Finalize\n"
|
||||
" Escape: Cancel\n\n"
|
||||
"COVER POINT TOOL:\n"
|
||||
" LMB: Place cover point\n"
|
||||
" Arrow = NPC facing direction"))
|
||||
.ColorAndOpacity(FSlateColor(FLinearColor(0.5f, 0.5f, 0.5f)))
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
FReply FPS_AI_Behavior_SplineEdModeToolkit::OnTypeButtonClicked(EPS_AI_Behavior_NPCType Type)
|
||||
{
|
||||
if (auto* Mode = GetSplineEdMode())
|
||||
{
|
||||
Mode->CurrentSplineType = Type;
|
||||
}
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
bool FPS_AI_Behavior_SplineEdModeToolkit::IsTypeSelected(EPS_AI_Behavior_NPCType Type) const
|
||||
{
|
||||
auto* Mode = GetSplineEdMode();
|
||||
return Mode && Mode->CurrentSplineType == Type;
|
||||
}
|
||||
|
||||
FSlateColor FPS_AI_Behavior_SplineEdModeToolkit::GetTypeColor(EPS_AI_Behavior_NPCType Type) const
|
||||
{
|
||||
const bool bSelected = IsTypeSelected(Type);
|
||||
const float Alpha = bSelected ? 1.0f : 0.4f;
|
||||
|
||||
switch (Type)
|
||||
{
|
||||
case EPS_AI_Behavior_NPCType::Civilian: return FSlateColor(FLinearColor(0.2f, 0.8f, 0.2f, Alpha));
|
||||
case EPS_AI_Behavior_NPCType::Enemy: return FSlateColor(FLinearColor(0.9f, 0.2f, 0.2f, Alpha));
|
||||
case EPS_AI_Behavior_NPCType::Protector: return FSlateColor(FLinearColor(0.2f, 0.4f, 1.0f, Alpha));
|
||||
case EPS_AI_Behavior_NPCType::Any: return FSlateColor(FLinearColor(1.0f, 0.7f, 0.0f, Alpha));
|
||||
default: return FSlateColor(FLinearColor(0.5f, 0.5f, 0.5f, Alpha));
|
||||
}
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
@ -0,0 +1,83 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#include "PS_AI_Behavior_SplineVisualizer.h"
|
||||
#include "PS_AI_Behavior_SplinePath.h"
|
||||
#include "Components/SplineComponent.h"
|
||||
|
||||
void FPS_AI_Behavior_SplineVisualizer::DrawVisualization(
|
||||
const UActorComponent* Component, const FSceneView* View, FPrimitiveDrawInterface* PDI)
|
||||
{
|
||||
const USplineComponent* SplineComp = Cast<USplineComponent>(Component);
|
||||
if (!SplineComp) return;
|
||||
|
||||
const APS_AI_Behavior_SplinePath* SplinePath = Cast<APS_AI_Behavior_SplinePath>(SplineComp->GetOwner());
|
||||
if (!SplinePath) return;
|
||||
|
||||
const float SplineLength = SplineComp->GetSplineLength();
|
||||
if (SplineLength <= 0.0f) return;
|
||||
|
||||
// ─── Draw direction arrows every 500cm ──────────────────────────────
|
||||
const float ArrowSpacing = 500.0f;
|
||||
for (float Dist = ArrowSpacing * 0.5f; Dist < SplineLength; Dist += ArrowSpacing)
|
||||
{
|
||||
const FVector Pos = SplineComp->GetLocationAtDistanceAlongSpline(Dist, ESplineCoordinateSpace::World);
|
||||
const FVector Dir = SplineComp->GetDirectionAtDistanceAlongSpline(Dist, ESplineCoordinateSpace::World);
|
||||
|
||||
const FVector ArrowEnd = Pos + Dir * 50.0f;
|
||||
const FVector Right = FVector::CrossProduct(Dir, FVector::UpVector).GetSafeNormal();
|
||||
|
||||
// Arrow shaft
|
||||
PDI->DrawLine(Pos - Dir * 20.0f, ArrowEnd, FLinearColor::White, SDPG_World, 1.5f);
|
||||
|
||||
// Arrow head
|
||||
PDI->DrawLine(ArrowEnd, ArrowEnd - Dir * 20.0f + Right * 12.0f,
|
||||
FLinearColor::White, SDPG_World, 1.5f);
|
||||
PDI->DrawLine(ArrowEnd, ArrowEnd - Dir * 20.0f - Right * 12.0f,
|
||||
FLinearColor::White, SDPG_World, 1.5f);
|
||||
|
||||
// Bidirectional? Draw reverse arrow too
|
||||
if (SplinePath->bBidirectional)
|
||||
{
|
||||
const FVector RevEnd = Pos - Dir * 50.0f;
|
||||
PDI->DrawLine(Pos + Dir * 20.0f, RevEnd, FLinearColor(0.5f, 0.5f, 0.5f), SDPG_World, 1.0f);
|
||||
PDI->DrawLine(RevEnd, RevEnd + Dir * 20.0f + Right * 10.0f,
|
||||
FLinearColor(0.5f, 0.5f, 0.5f), SDPG_World, 1.0f);
|
||||
PDI->DrawLine(RevEnd, RevEnd + Dir * 20.0f - Right * 10.0f,
|
||||
FLinearColor(0.5f, 0.5f, 0.5f), SDPG_World, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Draw distance markers every 1000cm (10m) ──────────────────────
|
||||
const float MarkerSpacing = 1000.0f;
|
||||
for (float Dist = MarkerSpacing; Dist < SplineLength; Dist += MarkerSpacing)
|
||||
{
|
||||
const FVector Pos = SplineComp->GetLocationAtDistanceAlongSpline(Dist, ESplineCoordinateSpace::World);
|
||||
|
||||
// Small cross marker
|
||||
PDI->DrawLine(Pos + FVector(15, 0, 0), Pos - FVector(15, 0, 0),
|
||||
FLinearColor(0.7f, 0.7f, 0.7f), SDPG_World, 1.0f);
|
||||
PDI->DrawLine(Pos + FVector(0, 15, 0), Pos - FVector(0, 15, 0),
|
||||
FLinearColor(0.7f, 0.7f, 0.7f), SDPG_World, 1.0f);
|
||||
}
|
||||
|
||||
// ─── Draw junctions as yellow/orange spheres ────────────────────────
|
||||
for (const FPS_AI_Behavior_SplineJunction& Junction : SplinePath->Junctions)
|
||||
{
|
||||
const FVector JuncPos = Junction.WorldLocation;
|
||||
|
||||
// Draw a star/cross shape at junction
|
||||
const float Size = 20.0f;
|
||||
const FLinearColor JuncColor = FLinearColor(1.0f, 0.9f, 0.0f); // Yellow
|
||||
|
||||
PDI->DrawLine(JuncPos + FVector(Size, 0, 0), JuncPos - FVector(Size, 0, 0), JuncColor, SDPG_Foreground, 3.0f);
|
||||
PDI->DrawLine(JuncPos + FVector(0, Size, 0), JuncPos - FVector(0, Size, 0), JuncColor, SDPG_Foreground, 3.0f);
|
||||
PDI->DrawLine(JuncPos + FVector(0, 0, Size), JuncPos - FVector(0, 0, Size), JuncColor, SDPG_Foreground, 3.0f);
|
||||
|
||||
// Line to the other spline's junction point
|
||||
if (Junction.OtherSpline.IsValid())
|
||||
{
|
||||
PDI->DrawLine(JuncPos, JuncPos + FVector(0, 0, 40.0f),
|
||||
FLinearColor(1.0f, 0.5f, 0.0f), SDPG_Foreground, 2.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,405 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#include "SPS_AI_Behavior_SplinePanel.h"
|
||||
#include "PS_AI_Behavior_SplinePath.h"
|
||||
#include "PS_AI_Behavior_SplineNetwork.h"
|
||||
#include "Components/SplineComponent.h"
|
||||
#include "Editor.h"
|
||||
#include "EngineUtils.h"
|
||||
#include "Engine/World.h"
|
||||
#include "Widgets/Input/SButton.h"
|
||||
#include "Widgets/Layout/SBorder.h"
|
||||
#include "Widgets/Layout/SScrollBox.h"
|
||||
#include "Widgets/Text/STextBlock.h"
|
||||
#include "Selection.h"
|
||||
#include "CollisionQueryParams.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "PS_AI_BehaviorSplinePanel"
|
||||
|
||||
const FName SPS_AI_Behavior_SplinePanel::TabId = FName("PS_AI_BehaviorSplinePanel");
|
||||
|
||||
void SPS_AI_Behavior_SplinePanel::Construct(const FArguments& InArgs)
|
||||
{
|
||||
ChildSlot
|
||||
[
|
||||
SNew(SVerticalBox)
|
||||
|
||||
// ─── Title ──────────────────────────────────────────────────
|
||||
+ SVerticalBox::Slot()
|
||||
.AutoHeight()
|
||||
.Padding(8)
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(LOCTEXT("PanelTitle", "PS AI Spline Network"))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 16))
|
||||
]
|
||||
|
||||
// ─── Creation Buttons ───────────────────────────────────────
|
||||
+ SVerticalBox::Slot()
|
||||
.AutoHeight()
|
||||
.Padding(8, 4)
|
||||
[
|
||||
SNew(SHorizontalBox)
|
||||
|
||||
+ SHorizontalBox::Slot().AutoWidth().Padding(0, 0, 4, 0)
|
||||
[
|
||||
SNew(SButton)
|
||||
.Text(LOCTEXT("NewCivilian", "+ Civilian"))
|
||||
.ButtonColorAndOpacity(FLinearColor(0.2f, 0.8f, 0.2f))
|
||||
.OnClicked(this, &SPS_AI_Behavior_SplinePanel::OnCreateSplineClicked, EPS_AI_Behavior_NPCType::Civilian)
|
||||
]
|
||||
|
||||
+ SHorizontalBox::Slot().AutoWidth().Padding(0, 0, 4, 0)
|
||||
[
|
||||
SNew(SButton)
|
||||
.Text(LOCTEXT("NewEnemy", "+ Enemy"))
|
||||
.ButtonColorAndOpacity(FLinearColor(0.9f, 0.2f, 0.2f))
|
||||
.OnClicked(this, &SPS_AI_Behavior_SplinePanel::OnCreateSplineClicked, EPS_AI_Behavior_NPCType::Enemy)
|
||||
]
|
||||
|
||||
+ SHorizontalBox::Slot().AutoWidth().Padding(0, 0, 4, 0)
|
||||
[
|
||||
SNew(SButton)
|
||||
.Text(LOCTEXT("NewProtector", "+ Protector"))
|
||||
.ButtonColorAndOpacity(FLinearColor(0.2f, 0.4f, 1.0f))
|
||||
.OnClicked(this, &SPS_AI_Behavior_SplinePanel::OnCreateSplineClicked, EPS_AI_Behavior_NPCType::Protector)
|
||||
]
|
||||
|
||||
+ SHorizontalBox::Slot().AutoWidth()
|
||||
[
|
||||
SNew(SButton)
|
||||
.Text(LOCTEXT("NewAny", "+ Any"))
|
||||
.ButtonColorAndOpacity(FLinearColor(1.0f, 0.7f, 0.0f))
|
||||
.OnClicked(this, &SPS_AI_Behavior_SplinePanel::OnCreateSplineClicked, EPS_AI_Behavior_NPCType::Any)
|
||||
]
|
||||
]
|
||||
|
||||
// ─── Action Buttons ─────────────────────────────────────────
|
||||
+ SVerticalBox::Slot()
|
||||
.AutoHeight()
|
||||
.Padding(8, 4)
|
||||
[
|
||||
SNew(SHorizontalBox)
|
||||
|
||||
+ SHorizontalBox::Slot().AutoWidth().Padding(0, 0, 4, 0)
|
||||
[
|
||||
SNew(SButton)
|
||||
.Text(LOCTEXT("Refresh", "Refresh List"))
|
||||
.OnClicked_Lambda([this]() { RefreshSplineList(); return FReply::Handled(); })
|
||||
]
|
||||
|
||||
+ SHorizontalBox::Slot().AutoWidth().Padding(0, 0, 4, 0)
|
||||
[
|
||||
SNew(SButton)
|
||||
.Text(LOCTEXT("RebuildNetwork", "Rebuild Junctions"))
|
||||
.OnClicked(this, &SPS_AI_Behavior_SplinePanel::OnRebuildNetworkClicked)
|
||||
]
|
||||
|
||||
+ SHorizontalBox::Slot().AutoWidth().Padding(0, 0, 4, 0)
|
||||
[
|
||||
SNew(SButton)
|
||||
.Text(LOCTEXT("Validate", "Validate Network"))
|
||||
.OnClicked(this, &SPS_AI_Behavior_SplinePanel::OnValidateNetworkClicked)
|
||||
]
|
||||
|
||||
+ SHorizontalBox::Slot().AutoWidth()
|
||||
[
|
||||
SNew(SButton)
|
||||
.Text(LOCTEXT("SnapGround", "Snap Selected to Ground"))
|
||||
.OnClicked(this, &SPS_AI_Behavior_SplinePanel::OnSnapSelectedToGroundClicked)
|
||||
]
|
||||
]
|
||||
|
||||
// ─── Spline List ────────────────────────────────────────────
|
||||
+ SVerticalBox::Slot()
|
||||
.FillHeight(1.0f)
|
||||
.Padding(8)
|
||||
[
|
||||
SAssignNew(SplineListView, SListView<TSharedPtr<FSplineListEntry>>)
|
||||
.ListItemsSource(&SplineEntries)
|
||||
.OnGenerateRow(this, &SPS_AI_Behavior_SplinePanel::GenerateSplineRow)
|
||||
.OnSelectionChanged(this, &SPS_AI_Behavior_SplinePanel::OnSplineSelected)
|
||||
.HeaderRow(
|
||||
SNew(SHeaderRow)
|
||||
+ SHeaderRow::Column("Name").DefaultLabel(LOCTEXT("ColName", "Name")).FillWidth(0.3f)
|
||||
+ SHeaderRow::Column("Type").DefaultLabel(LOCTEXT("ColType", "Type")).FillWidth(0.15f)
|
||||
+ SHeaderRow::Column("Length").DefaultLabel(LOCTEXT("ColLength", "Length")).FillWidth(0.15f)
|
||||
+ SHeaderRow::Column("Points").DefaultLabel(LOCTEXT("ColPoints", "Pts")).FillWidth(0.1f)
|
||||
+ SHeaderRow::Column("Junctions").DefaultLabel(LOCTEXT("ColJunctions", "Junctions")).FillWidth(0.1f)
|
||||
)
|
||||
]
|
||||
];
|
||||
|
||||
RefreshSplineList();
|
||||
}
|
||||
|
||||
void SPS_AI_Behavior_SplinePanel::RefreshSplineList()
|
||||
{
|
||||
SplineEntries.Empty();
|
||||
|
||||
UWorld* World = GetEditorWorld();
|
||||
if (!World) return;
|
||||
|
||||
for (TActorIterator<APS_AI_Behavior_SplinePath> It(World); It; ++It)
|
||||
{
|
||||
APS_AI_Behavior_SplinePath* Spline = *It;
|
||||
if (!Spline) continue;
|
||||
|
||||
TSharedPtr<FSplineListEntry> Entry = MakeShared<FSplineListEntry>();
|
||||
Entry->Spline = Spline;
|
||||
Entry->Name = Spline->GetActorLabel().IsEmpty() ? Spline->GetName() : Spline->GetActorLabel();
|
||||
Entry->Type = Spline->SplineCategory;
|
||||
Entry->Length = Spline->GetSplineLength();
|
||||
Entry->JunctionCount = Spline->Junctions.Num();
|
||||
Entry->PointCount = Spline->SplineComp ? Spline->SplineComp->GetNumberOfSplinePoints() : 0;
|
||||
|
||||
SplineEntries.Add(Entry);
|
||||
}
|
||||
|
||||
if (SplineListView.IsValid())
|
||||
{
|
||||
SplineListView->RequestListRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
TSharedRef<ITableRow> SPS_AI_Behavior_SplinePanel::GenerateSplineRow(
|
||||
TSharedPtr<FSplineListEntry> Entry,
|
||||
const TSharedRef<STableViewBase>& OwnerTable)
|
||||
{
|
||||
const FLinearColor TypeColor = GetColorForType(Entry->Type);
|
||||
|
||||
return SNew(STableRow<TSharedPtr<FSplineListEntry>>, OwnerTable)
|
||||
[
|
||||
SNew(SHorizontalBox)
|
||||
|
||||
+ SHorizontalBox::Slot().FillWidth(0.3f).Padding(4, 2)
|
||||
[
|
||||
SNew(STextBlock).Text(FText::FromString(Entry->Name))
|
||||
]
|
||||
|
||||
+ SHorizontalBox::Slot().FillWidth(0.15f).Padding(4, 2)
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(FText::FromString(UEnum::GetDisplayValueAsText(Entry->Type).ToString()))
|
||||
.ColorAndOpacity(FSlateColor(TypeColor))
|
||||
]
|
||||
|
||||
+ SHorizontalBox::Slot().FillWidth(0.15f).Padding(4, 2)
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(FText::FromString(FString::Printf(TEXT("%.0f cm"), Entry->Length)))
|
||||
]
|
||||
|
||||
+ SHorizontalBox::Slot().FillWidth(0.1f).Padding(4, 2)
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(FText::FromString(FString::FromInt(Entry->PointCount)))
|
||||
]
|
||||
|
||||
+ SHorizontalBox::Slot().FillWidth(0.1f).Padding(4, 2)
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(FText::FromString(FString::FromInt(Entry->JunctionCount)))
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
void SPS_AI_Behavior_SplinePanel::OnSplineSelected(
|
||||
TSharedPtr<FSplineListEntry> Entry, ESelectInfo::Type SelectInfo)
|
||||
{
|
||||
if (!Entry.IsValid() || !Entry->Spline.IsValid()) return;
|
||||
|
||||
// Select in editor and focus
|
||||
GEditor->SelectNone(true, true);
|
||||
GEditor->SelectActor(Entry->Spline.Get(), true, true);
|
||||
GEditor->MoveViewportCamerasToActor(*Entry->Spline.Get(), false);
|
||||
}
|
||||
|
||||
FReply SPS_AI_Behavior_SplinePanel::OnCreateSplineClicked(EPS_AI_Behavior_NPCType Type)
|
||||
{
|
||||
UWorld* World = GetEditorWorld();
|
||||
if (!World) return FReply::Handled();
|
||||
|
||||
// Get viewport camera location as spawn point
|
||||
FVector SpawnLoc = FVector::ZeroVector;
|
||||
if (GEditor && GEditor->GetActiveViewport())
|
||||
{
|
||||
FEditorViewportClient* ViewportClient = static_cast<FEditorViewportClient*>(
|
||||
GEditor->GetActiveViewport()->GetClient());
|
||||
if (ViewportClient)
|
||||
{
|
||||
SpawnLoc = ViewportClient->GetViewLocation() + ViewportClient->GetViewRotation().Vector() * 500.0f;
|
||||
}
|
||||
}
|
||||
|
||||
GEditor->BeginTransaction(FText::FromString(TEXT("Create Spline Path")));
|
||||
|
||||
FActorSpawnParameters SpawnParams;
|
||||
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
||||
|
||||
APS_AI_Behavior_SplinePath* NewSpline = World->SpawnActor<APS_AI_Behavior_SplinePath>(
|
||||
APS_AI_Behavior_SplinePath::StaticClass(), FTransform(SpawnLoc), SpawnParams);
|
||||
|
||||
if (NewSpline)
|
||||
{
|
||||
NewSpline->SplineCategory = Type;
|
||||
const FString TypeName = UEnum::GetDisplayValueAsText(Type).ToString();
|
||||
NewSpline->SetActorLabel(FString::Printf(TEXT("SplinePath_%s"), *TypeName));
|
||||
NewSpline->Modify();
|
||||
|
||||
GEditor->SelectNone(true, true);
|
||||
GEditor->SelectActor(NewSpline, true, true);
|
||||
}
|
||||
|
||||
GEditor->EndTransaction();
|
||||
RefreshSplineList();
|
||||
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
FReply SPS_AI_Behavior_SplinePanel::OnRebuildNetworkClicked()
|
||||
{
|
||||
UWorld* World = GetEditorWorld();
|
||||
if (!World) return FReply::Handled();
|
||||
|
||||
UPS_AI_Behavior_SplineNetwork* Network = World->GetSubsystem<UPS_AI_Behavior_SplineNetwork>();
|
||||
if (Network)
|
||||
{
|
||||
Network->RebuildNetwork();
|
||||
}
|
||||
|
||||
RefreshSplineList();
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
FReply SPS_AI_Behavior_SplinePanel::OnValidateNetworkClicked()
|
||||
{
|
||||
UWorld* World = GetEditorWorld();
|
||||
if (!World) return FReply::Handled();
|
||||
|
||||
int32 OrphanCount = 0;
|
||||
int32 TooShortCount = 0;
|
||||
int32 SinglePointCount = 0;
|
||||
|
||||
for (TActorIterator<APS_AI_Behavior_SplinePath> It(World); It; ++It)
|
||||
{
|
||||
APS_AI_Behavior_SplinePath* Spline = *It;
|
||||
if (!Spline) continue;
|
||||
|
||||
if (Spline->Junctions.Num() == 0)
|
||||
{
|
||||
++OrphanCount;
|
||||
UE_LOG(LogPS_AI_Behavior, Warning,
|
||||
TEXT("Validation: Orphan spline '%s' has no junctions."),
|
||||
*Spline->GetActorLabel());
|
||||
}
|
||||
|
||||
if (Spline->GetSplineLength() < 100.0f)
|
||||
{
|
||||
++TooShortCount;
|
||||
UE_LOG(LogPS_AI_Behavior, Warning,
|
||||
TEXT("Validation: Spline '%s' is too short (%.0f cm)."),
|
||||
*Spline->GetActorLabel(), Spline->GetSplineLength());
|
||||
}
|
||||
|
||||
if (Spline->SplineComp && Spline->SplineComp->GetNumberOfSplinePoints() < 2)
|
||||
{
|
||||
++SinglePointCount;
|
||||
UE_LOG(LogPS_AI_Behavior, Warning,
|
||||
TEXT("Validation: Spline '%s' has only %d point(s)."),
|
||||
*Spline->GetActorLabel(), Spline->SplineComp->GetNumberOfSplinePoints());
|
||||
}
|
||||
}
|
||||
|
||||
if (OrphanCount == 0 && TooShortCount == 0 && SinglePointCount == 0)
|
||||
{
|
||||
UE_LOG(LogPS_AI_Behavior, Log, TEXT("Validation: Network OK — no issues found."));
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogPS_AI_Behavior, Warning,
|
||||
TEXT("Validation: %d orphan(s), %d too short, %d single-point."),
|
||||
OrphanCount, TooShortCount, SinglePointCount);
|
||||
}
|
||||
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
FReply SPS_AI_Behavior_SplinePanel::OnSnapSelectedToGroundClicked()
|
||||
{
|
||||
UWorld* World = GetEditorWorld();
|
||||
if (!World) return FReply::Handled();
|
||||
|
||||
// Get selected actors
|
||||
TArray<APS_AI_Behavior_SplinePath*> SelectedSplines;
|
||||
for (FSelectionIterator It(GEditor->GetSelectedActorIterator()); It; ++It)
|
||||
{
|
||||
if (APS_AI_Behavior_SplinePath* Spline = Cast<APS_AI_Behavior_SplinePath>(*It))
|
||||
{
|
||||
SelectedSplines.Add(Spline);
|
||||
}
|
||||
}
|
||||
|
||||
if (SelectedSplines.Num() == 0)
|
||||
{
|
||||
UE_LOG(LogPS_AI_Behavior, Warning, TEXT("Snap to Ground: No SplinePath actors selected."));
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
GEditor->BeginTransaction(FText::FromString(TEXT("Snap Spline Points to Ground")));
|
||||
|
||||
for (APS_AI_Behavior_SplinePath* Spline : SelectedSplines)
|
||||
{
|
||||
if (!Spline->SplineComp) continue;
|
||||
Spline->Modify();
|
||||
|
||||
const int32 NumPoints = Spline->SplineComp->GetNumberOfSplinePoints();
|
||||
for (int32 i = 0; i < NumPoints; ++i)
|
||||
{
|
||||
FVector PointLoc = Spline->SplineComp->GetLocationAtSplinePoint(i, ESplineCoordinateSpace::World);
|
||||
|
||||
// Trace down
|
||||
FHitResult Hit;
|
||||
FCollisionQueryParams Params(SCENE_QUERY_STAT(SnapSplineToGround), true);
|
||||
Params.AddIgnoredActor(Spline);
|
||||
|
||||
const FVector TraceStart = PointLoc + FVector(0, 0, 500.0f);
|
||||
const FVector TraceEnd = PointLoc - FVector(0, 0, 5000.0f);
|
||||
|
||||
if (World->LineTraceSingleByChannel(Hit, TraceStart, TraceEnd, ECC_WorldStatic, Params))
|
||||
{
|
||||
const FVector NewLoc = Hit.ImpactPoint + FVector(0, 0, 5.0f);
|
||||
Spline->SplineComp->SetLocationAtSplinePoint(i, NewLoc, ESplineCoordinateSpace::World, true);
|
||||
}
|
||||
}
|
||||
|
||||
Spline->SplineComp->UpdateSpline();
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Log, TEXT("Snapped %d points of '%s' to ground."),
|
||||
NumPoints, *Spline->GetActorLabel());
|
||||
}
|
||||
|
||||
GEditor->EndTransaction();
|
||||
RefreshSplineList();
|
||||
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
UWorld* SPS_AI_Behavior_SplinePanel::GetEditorWorld() const
|
||||
{
|
||||
return GEditor ? GEditor->GetEditorWorldContext().World() : nullptr;
|
||||
}
|
||||
|
||||
FLinearColor SPS_AI_Behavior_SplinePanel::GetColorForType(EPS_AI_Behavior_NPCType Type) const
|
||||
{
|
||||
switch (Type)
|
||||
{
|
||||
case EPS_AI_Behavior_NPCType::Civilian: return FLinearColor(0.2f, 0.8f, 0.2f);
|
||||
case EPS_AI_Behavior_NPCType::Enemy: return FLinearColor(0.9f, 0.2f, 0.2f);
|
||||
case EPS_AI_Behavior_NPCType::Protector: return FLinearColor(0.2f, 0.4f, 1.0f);
|
||||
case EPS_AI_Behavior_NPCType::Any: return FLinearColor(1.0f, 0.7f, 0.0f);
|
||||
default: return FLinearColor::White;
|
||||
}
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
@ -0,0 +1,25 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Modules/ModuleManager.h"
|
||||
|
||||
class FPS_AI_BehaviorEditorModule : public IModuleInterface
|
||||
{
|
||||
public:
|
||||
virtual void StartupModule() override;
|
||||
virtual void ShutdownModule() override;
|
||||
|
||||
private:
|
||||
void RegisterEdMode();
|
||||
void UnregisterEdMode();
|
||||
|
||||
void RegisterVisualizer();
|
||||
void UnregisterVisualizer();
|
||||
|
||||
void RegisterDetailCustomizations();
|
||||
void UnregisterDetailCustomizations();
|
||||
|
||||
void RegisterSplinePanel();
|
||||
void UnregisterSplinePanel();
|
||||
};
|
||||
@ -0,0 +1,102 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "EdMode.h"
|
||||
#include "PS_AI_Behavior_Definitions.h"
|
||||
|
||||
class APS_AI_Behavior_SplinePath;
|
||||
class APS_AI_Behavior_CoverPoint;
|
||||
class USplineComponent;
|
||||
|
||||
/** Active tool within the EdMode. */
|
||||
enum class EPS_AI_Behavior_EdModeTool : uint8
|
||||
{
|
||||
Spline, // Place spline points
|
||||
CoverPoint, // Place cover points / hiding spots
|
||||
};
|
||||
|
||||
/**
|
||||
* Editor Mode for interactive placement of splines and cover points.
|
||||
* Activated from the toolbar. Supports two tools:
|
||||
* - Spline: click to add points, Enter to finalize
|
||||
* - CoverPoint: click to place, arrow shows facing direction
|
||||
*/
|
||||
class FPS_AI_Behavior_SplineEdMode : public FEdMode
|
||||
{
|
||||
public:
|
||||
static const FEditorModeID EM_SplineEdModeId;
|
||||
|
||||
FPS_AI_Behavior_SplineEdMode();
|
||||
virtual ~FPS_AI_Behavior_SplineEdMode();
|
||||
|
||||
// ─── FEdMode Interface ──────────────────────────────────────────────
|
||||
|
||||
virtual void Enter() override;
|
||||
virtual void Exit() override;
|
||||
|
||||
virtual bool HandleClick(FEditorViewportClient* InViewportClient,
|
||||
HHitProxy* HitProxy, const FViewportClick& Click) override;
|
||||
virtual bool InputKey(FEditorViewportClient* ViewportClient,
|
||||
FViewport* Viewport, FKey Key, EInputEvent Event) override;
|
||||
|
||||
virtual void Render(const FSceneView* View, FViewport* Viewport, FPrimitiveDrawInterface* PDI) override;
|
||||
virtual bool UsesToolkits() const override { return true; }
|
||||
virtual bool IsCompatibleWith(FEditorModeID OtherModeID) const override;
|
||||
|
||||
// ─── Active Tool ────────────────────────────────────────────────────
|
||||
|
||||
/** Which tool is currently active. */
|
||||
EPS_AI_Behavior_EdModeTool ActiveTool = EPS_AI_Behavior_EdModeTool::Spline;
|
||||
|
||||
// ─── Spline Placement ───────────────────────────────────────────────
|
||||
|
||||
/** The type of spline currently being placed. */
|
||||
EPS_AI_Behavior_NPCType CurrentSplineType = EPS_AI_Behavior_NPCType::Civilian;
|
||||
|
||||
/** Whether to snap placed points to the ground. */
|
||||
bool bSnapToGround = true;
|
||||
|
||||
/** Whether to show junction preview in the viewport. */
|
||||
bool bShowJunctionPreview = true;
|
||||
|
||||
/** Finalize the current spline and start a new one. */
|
||||
void FinalizeCurrentSpline();
|
||||
|
||||
/** Get the spline currently being built (can be null). */
|
||||
APS_AI_Behavior_SplinePath* GetActiveSpline() const { return ActiveSpline; }
|
||||
|
||||
/** Select an existing spline for extension. */
|
||||
void SelectSplineForExtension(APS_AI_Behavior_SplinePath* Spline);
|
||||
|
||||
// ─── Cover Point Placement ──────────────────────────────────────────
|
||||
|
||||
/** Type of cover point to place. */
|
||||
EPS_AI_Behavior_CoverPointType CurrentCoverType = EPS_AI_Behavior_CoverPointType::Cover;
|
||||
|
||||
/** NPC type restriction for newly placed cover points. */
|
||||
EPS_AI_Behavior_NPCType CoverAllowedNPCType = EPS_AI_Behavior_NPCType::Any;
|
||||
|
||||
private:
|
||||
/** The spline actor being built. */
|
||||
APS_AI_Behavior_SplinePath* ActiveSpline = nullptr;
|
||||
|
||||
/** Number of points added to the active spline. */
|
||||
int32 PointCount = 0;
|
||||
|
||||
/** Spawn a new SplinePath actor of the current type. */
|
||||
APS_AI_Behavior_SplinePath* SpawnNewSpline(const FVector& FirstPoint);
|
||||
|
||||
/** Add a point to the active spline. */
|
||||
void AddPointToSpline(const FVector& WorldLocation);
|
||||
|
||||
/** Place a cover point at the given location facing the camera. */
|
||||
APS_AI_Behavior_CoverPoint* PlaceCoverPoint(const FVector& WorldLocation, const FRotator& Facing);
|
||||
|
||||
/** Snap a world location to the ground via line trace. */
|
||||
bool SnapToGround(FVector& InOutLocation) const;
|
||||
|
||||
/** Rebuild the spline network preview. */
|
||||
void RebuildNetworkPreview();
|
||||
};
|
||||
@ -0,0 +1,43 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Toolkits/BaseToolkit.h"
|
||||
#include "PS_AI_Behavior_Definitions.h"
|
||||
|
||||
class FPS_AI_Behavior_SplineEdMode;
|
||||
|
||||
/**
|
||||
* Toolkit (toolbar widget) for the Spline EdMode.
|
||||
* Shows buttons for spline type selection, snap toggle, and preview toggle.
|
||||
*/
|
||||
class FPS_AI_Behavior_SplineEdModeToolkit : public FModeToolkit
|
||||
{
|
||||
public:
|
||||
virtual void Init(const TSharedPtr<IToolkitHost>& InitToolkitHost) override;
|
||||
|
||||
virtual FName GetToolkitFName() const override { return FName("PS_AI_BehaviorSplineEdModeToolkit"); }
|
||||
virtual FText GetBaseToolkitName() const override { return FText::FromString("PS AI Spline"); }
|
||||
|
||||
virtual class FEdMode* GetEditorMode() const override;
|
||||
|
||||
virtual TSharedPtr<SWidget> GetInlineContent() const override { return ToolkitWidget; }
|
||||
|
||||
private:
|
||||
TSharedPtr<SWidget> ToolkitWidget;
|
||||
|
||||
FPS_AI_Behavior_SplineEdMode* GetSplineEdMode() const;
|
||||
|
||||
/** Build the toolbar widget. */
|
||||
TSharedRef<SWidget> BuildToolkitWidget();
|
||||
|
||||
/** Callbacks for type buttons. */
|
||||
FReply OnTypeButtonClicked(EPS_AI_Behavior_NPCType Type);
|
||||
|
||||
/** Is this type currently selected? */
|
||||
bool IsTypeSelected(EPS_AI_Behavior_NPCType Type) const;
|
||||
|
||||
/** Get color for a type. */
|
||||
FSlateColor GetTypeColor(EPS_AI_Behavior_NPCType Type) const;
|
||||
};
|
||||
@ -0,0 +1,19 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "ComponentVisualizer.h"
|
||||
|
||||
class USplineComponent;
|
||||
|
||||
/**
|
||||
* Component Visualizer for SplinePath's SplineComponent.
|
||||
* Draws junctions, direction arrows, and distance markers in the editor viewport.
|
||||
*/
|
||||
class FPS_AI_Behavior_SplineVisualizer : public FComponentVisualizer
|
||||
{
|
||||
public:
|
||||
virtual void DrawVisualization(const UActorComponent* Component,
|
||||
const FSceneView* View, FPrimitiveDrawInterface* PDI) override;
|
||||
};
|
||||
@ -0,0 +1,65 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Widgets/SCompoundWidget.h"
|
||||
#include "Widgets/Views/SListView.h"
|
||||
#include "PS_AI_Behavior_Definitions.h"
|
||||
|
||||
class APS_AI_Behavior_SplinePath;
|
||||
|
||||
/** Row data for the spline list. */
|
||||
struct FSplineListEntry
|
||||
{
|
||||
TWeakObjectPtr<APS_AI_Behavior_SplinePath> Spline;
|
||||
FString Name;
|
||||
EPS_AI_Behavior_NPCType Type;
|
||||
float Length;
|
||||
int32 JunctionCount;
|
||||
int32 PointCount;
|
||||
};
|
||||
|
||||
/**
|
||||
* Dockable panel for managing spline paths.
|
||||
* Shows a list of all splines, creation buttons, validation, and network rebuild.
|
||||
*/
|
||||
class SPS_AI_Behavior_SplinePanel : public SCompoundWidget
|
||||
{
|
||||
public:
|
||||
SLATE_BEGIN_ARGS(SPS_AI_Behavior_SplinePanel) {}
|
||||
SLATE_END_ARGS()
|
||||
|
||||
void Construct(const FArguments& InArgs);
|
||||
|
||||
static const FName TabId;
|
||||
|
||||
private:
|
||||
// ─── List ───────────────────────────────────────────────────────────
|
||||
|
||||
TArray<TSharedPtr<FSplineListEntry>> SplineEntries;
|
||||
TSharedPtr<SListView<TSharedPtr<FSplineListEntry>>> SplineListView;
|
||||
|
||||
/** Refresh the spline list from the world. */
|
||||
void RefreshSplineList();
|
||||
|
||||
/** Generate a row widget for the list. */
|
||||
TSharedRef<ITableRow> GenerateSplineRow(
|
||||
TSharedPtr<FSplineListEntry> Entry,
|
||||
const TSharedRef<STableViewBase>& OwnerTable);
|
||||
|
||||
/** Handle selection — focus viewport on the spline. */
|
||||
void OnSplineSelected(TSharedPtr<FSplineListEntry> Entry, ESelectInfo::Type SelectInfo);
|
||||
|
||||
// ─── Actions ────────────────────────────────────────────────────────
|
||||
|
||||
FReply OnCreateSplineClicked(EPS_AI_Behavior_NPCType Type);
|
||||
FReply OnRebuildNetworkClicked();
|
||||
FReply OnValidateNetworkClicked();
|
||||
FReply OnSnapSelectedToGroundClicked();
|
||||
|
||||
// ─── Helpers ────────────────────────────────────────────────────────
|
||||
|
||||
UWorld* GetEditorWorld() const;
|
||||
FLinearColor GetColorForType(EPS_AI_Behavior_NPCType Type) const;
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user