Compare commits
No commits in common. "909583a1bbb5614b8f032093a54d5ca9128c214f" and "a07ac366868d03426b3cd8fa0709fd9848a45a05" have entirely different histories.
909583a1bb
...
a07ac36686
@ -40,10 +40,6 @@
|
|||||||
{
|
{
|
||||||
"Name": "AudioCapture",
|
"Name": "AudioCapture",
|
||||||
"Enabled": true
|
"Enabled": true
|
||||||
},
|
|
||||||
{
|
|
||||||
"Name": "PS_AI_Behavior",
|
|
||||||
"Enabled": true
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -1,511 +0,0 @@
|
|||||||
# 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**
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
{
|
|
||||||
"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": []
|
|
||||||
}
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
// 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",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
// 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);
|
|
||||||
}
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
// 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.");
|
|
||||||
}
|
|
||||||
@ -1,70 +0,0 @@
|
|||||||
// 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.");
|
|
||||||
}
|
|
||||||
@ -1,147 +0,0 @@
|
|||||||
// 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.");
|
|
||||||
}
|
|
||||||
@ -1,152 +0,0 @@
|
|||||||
// 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(""));
|
|
||||||
}
|
|
||||||
@ -1,271 +0,0 @@
|
|||||||
// 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);
|
|
||||||
}
|
|
||||||
@ -1,135 +0,0 @@
|
|||||||
// 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);
|
|
||||||
}
|
|
||||||
@ -1,127 +0,0 @@
|
|||||||
// 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(""));
|
|
||||||
}
|
|
||||||
@ -1,130 +0,0 @@
|
|||||||
// 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);
|
|
||||||
}
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,86 +0,0 @@
|
|||||||
// 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")));
|
|
||||||
}
|
|
||||||
@ -1,89 +0,0 @@
|
|||||||
// 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));
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
// 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
|
|
||||||
@ -1,357 +0,0 @@
|
|||||||
// 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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,115 +0,0 @@
|
|||||||
// 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);
|
|
||||||
}
|
|
||||||
@ -1,194 +0,0 @@
|
|||||||
// 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
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
// Copyright Asterion. All Rights Reserved.
|
|
||||||
|
|
||||||
#include "PS_AI_Behavior_Definitions.h"
|
|
||||||
|
|
||||||
DEFINE_LOG_CATEGORY(LogPS_AI_Behavior);
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
// 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".
|
|
||||||
@ -1,326 +0,0 @@
|
|||||||
// 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;
|
|
||||||
}
|
|
||||||
@ -1,189 +0,0 @@
|
|||||||
// 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;
|
|
||||||
}
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
// 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());
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
// Copyright Asterion. All Rights Reserved.
|
|
||||||
|
|
||||||
#include "PS_AI_Behavior_Settings.h"
|
|
||||||
|
|
||||||
UPS_AI_Behavior_Settings::UPS_AI_Behavior_Settings()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
@ -1,311 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,257 +0,0 @@
|
|||||||
// 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;
|
|
||||||
}
|
|
||||||
@ -1,141 +0,0 @@
|
|||||||
// 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
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
// 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;
|
|
||||||
};
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
// 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;
|
|
||||||
};
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
// 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;
|
|
||||||
};
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
// 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); }
|
|
||||||
};
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
// 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); }
|
|
||||||
};
|
|
||||||
@ -1,93 +0,0 @@
|
|||||||
// 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;
|
|
||||||
};
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
// 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;
|
|
||||||
};
|
|
||||||
@ -1,56 +0,0 @@
|
|||||||
// 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); }
|
|
||||||
};
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
// 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); }
|
|
||||||
};
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
// 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;
|
|
||||||
};
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
// 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;
|
|
||||||
};
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
// 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;
|
|
||||||
};
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
// 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;
|
|
||||||
};
|
|
||||||
@ -1,116 +0,0 @@
|
|||||||
// 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();
|
|
||||||
};
|
|
||||||
@ -1,103 +0,0 @@
|
|||||||
// 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;
|
|
||||||
};
|
|
||||||
@ -1,131 +0,0 @@
|
|||||||
// 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();
|
|
||||||
};
|
|
||||||
@ -1,98 +0,0 @@
|
|||||||
// 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");
|
|
||||||
}
|
|
||||||
@ -1,114 +0,0 @@
|
|||||||
// 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);
|
|
||||||
};
|
|
||||||
@ -1,73 +0,0 @@
|
|||||||
// 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();
|
|
||||||
};
|
|
||||||
@ -1,121 +0,0 @@
|
|||||||
// 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);
|
|
||||||
};
|
|
||||||
@ -1,121 +0,0 @@
|
|||||||
// 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;
|
|
||||||
};
|
|
||||||
@ -1,54 +0,0 @@
|
|||||||
// 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"); }
|
|
||||||
};
|
|
||||||
@ -1,169 +0,0 @@
|
|||||||
// 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;
|
|
||||||
};
|
|
||||||
@ -1,108 +0,0 @@
|
|||||||
// 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;
|
|
||||||
};
|
|
||||||
@ -1,144 +0,0 @@
|
|||||||
// 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();
|
|
||||||
};
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
// 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",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,134 +0,0 @@
|
|||||||
// 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
|
|
||||||
@ -1,433 +0,0 @@
|
|||||||
// 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,329 +0,0 @@
|
|||||||
// 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
|
|
||||||
@ -1,83 +0,0 @@
|
|||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,405 +0,0 @@
|
|||||||
// 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
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
// 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();
|
|
||||||
};
|
|
||||||
@ -1,102 +0,0 @@
|
|||||||
// 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();
|
|
||||||
};
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
// 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;
|
|
||||||
};
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
// 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;
|
|
||||||
};
|
|
||||||
@ -1,65 +0,0 @@
|
|||||||
// 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