Olympe Engine 2.0
2D Game Engine with ECS Architecture
Loading...
Searching...
No Matches
ECS_Systems_AI.cpp
Go to the documentation of this file.
1/*
2Olympe Engine V2 - 2025
3Nicolas Chereau
4nchereau@gmail.com
5
6This file is part of Olympe Engine V2.
7
8AI Systems implementation: NPC AI behavior systems.
9
10*/
11
12#include "ECS_Systems_AI.h"
13#include "ECS_Components.h"
14#include "ECS_Components_AI.h"
15#include "AI/BehaviorTree.h"
17#include "World.h"
18#include "GameEngine.h"
19#include "system/EventQueue.h"
20#include "system/system_utils.h"
21#include <cmath>
22#include <algorithm>
23
24// Forward declaration of global debugger instance (defined in OlympeEngine.cpp)
26
27// --- AIStimuliSystem Implementation ---
28
30{
31 // Requires AIBlackboard component
33}
34
36{
37 // Early return if no entities
38 if (m_entities.empty())
39 return;
40
43
44 // Process Gameplay domain events for AI stimuli
45 queue.ForEachDomainEvent(EventDomain::Gameplay, [&](const Message& msg) {
46
47 // Handle damage/hit events
48 if (msg.msg_type == EventType::EventType_Hit)
49 {
50 // Find AI entities that should react to this damage
51 for (EntityID entity : m_entities)
52 {
53 try
54 {
55 AIBlackboard_data& blackboard = World::Get().GetComponent<AIBlackboard_data>(entity);
56
57 // If this entity was hit (targetUid matches)
58 if (msg.targetUid == entity)
59 {
60 blackboard.lastDamageTaken = currentTime;
61 blackboard.damageAmount = msg.param1;
62
63 // Set attacker as target if we don't have one
64 if (!blackboard.hasTarget && msg.deviceId > 0)
65 {
66 EntityID attacker = static_cast<EntityID>(msg.deviceId);
67 if (World::Get().IsEntityValid(attacker))
68 {
69 blackboard.targetEntity = attacker;
70 blackboard.hasTarget = true;
71
72 if (World::Get().HasComponent<Position_data>(attacker))
73 {
74 const Position_data& attackerPos = World::Get().GetComponent<Position_data>(attacker);
75 blackboard.lastKnownTargetPosition = attackerPos.position;
76 }
77 }
78 }
79 }
80 }
81 catch (...)
82 {
83 // Entity may have been destroyed
84 }
85 }
86 }
87
88 // Handle explosion/noise events (using Game_TakeScreenshot as proxy for explosion)
89 // In a real implementation, you'd add specific event types
91 {
92 // Treat as explosion/noise event
93 Vector noisePos(msg.param1, msg.param2, 0.0f);
94 float noiseRadius = 500.0f; // Default hearing radius
95
96 for (EntityID entity : m_entities)
97 {
98 try
99 {
100 if (!World::Get().HasComponent<Position_data>(entity)) continue;
101 if (!World::Get().HasComponent<AISenses_data>(entity)) continue;
102
103 Position_data& pos = World::Get().GetComponent<Position_data>(entity);
104 AISenses_data& senses = World::Get().GetComponent<AISenses_data>(entity);
105 AIBlackboard_data& blackboard = World::Get().GetComponent<AIBlackboard_data>(entity);
106
107 float distance = (pos.position - noisePos).Magnitude();
108 if (distance <= senses.hearingRadius)
109 {
110 blackboard.heardNoise = true;
111 blackboard.lastNoisePosition = noisePos;
112 blackboard.noiseCooldown = 3.0f; // Hear noise for 3 seconds
113 }
114 }
115 catch (...)
116 {
117 // Entity may have been destroyed
118 }
119 }
120 }
121 });
122
123 // Update noise cooldowns
124 for (EntityID entity : m_entities)
125 {
126 try
127 {
129
130 if (blackboard.heardNoise)
131 {
133 if (blackboard.noiseCooldown <= 0.0f)
134 {
135 blackboard.heardNoise = false;
136 }
137 }
138 }
139 catch (...)
140 {
141 // Entity may have been destroyed
142 }
143 }
144}
145
146// --- AIPerceptionSystem Implementation ---
147
155
157{
158 // Early return if no entities
159 if (m_entities.empty())
160 return;
161
163
164 for (EntityID entity : m_entities)
165 {
166 try
167 {
171
172 // Timeslicing: only update perception at specified Hz
173 if (currentTime < senses.nextPerceptionTime)
174 continue;
175
176 senses.nextPerceptionTime = currentTime + (1.0f / senses.perceptionHz);
177
178 // Update target tracking if we have a target
179 if (blackboard.hasTarget && blackboard.targetEntity != INVALID_ENTITY_ID)
180 {
181 if (!World::Get().IsEntityValid(blackboard.targetEntity))
182 {
183 // Target was destroyed
184 blackboard.hasTarget = false;
185 blackboard.targetEntity = INVALID_ENTITY_ID;
186 blackboard.targetVisible = false;
187 continue;
188 }
189
190 // Check if target is still visible
191 bool visible = IsTargetVisible(entity, blackboard.targetEntity,
192 senses.visionRadius, senses.visionAngle);
193
194 blackboard.targetVisible = visible;
195
196 if (visible)
197 {
198 // Update last known position
200 {
202 blackboard.lastKnownTargetPosition = targetPos.position;
203 blackboard.timeSinceTargetSeen = 0.0f;
204
205 // Update distance
206 blackboard.distanceToTarget = Vector(targetPos.position - pos.position).Magnitude();
207 }
208 }
209 else
210 {
211 blackboard.timeSinceTargetSeen += (1.0f / senses.perceptionHz);
212
213 // Lose target after 5 seconds of not seeing them
214 if (blackboard.timeSinceTargetSeen > 5.0f)
215 {
216 blackboard.hasTarget = false;
217 blackboard.targetEntity = INVALID_ENTITY_ID;
218 }
219 }
220 }
221 else
222 {
223 // No current target - scan for potential targets
224 // Naive scan: check all entities with PlayerBinding_data (players are potential targets)
225 for (const auto& kv : World::Get().m_entitySignatures)
226 {
228 if (potentialTarget == entity) continue;
229
230 // Only target entities with PlayerBinding_data (players)
232 continue;
233
234 if (IsTargetVisible(entity, potentialTarget, senses.visionRadius, senses.visionAngle))
235 {
236 // Found a visible target!
237 blackboard.hasTarget = true;
238 blackboard.targetEntity = potentialTarget;
239 blackboard.targetVisible = true;
240 blackboard.timeSinceTargetSeen = 0.0f;
241
243 {
245 blackboard.lastKnownTargetPosition = targetPos.position;
246 blackboard.distanceToTarget = (targetPos.position - pos.position).Magnitude();
247 }
248
249 break; // Only acquire one target at a time
250 }
251 }
252 }
253 }
254 catch (const std::exception& e)
255 {
256 SYSTEM_LOG << "AIPerceptionSystem Error for Entity " << entity << ": " << e.what() << "\n";
257 }
258 }
259}
260
261bool AIPerceptionSystem::IsTargetVisible(EntityID entity, EntityID target, float visionRadius, float visionAngle)
262{
263 if (!World::Get().HasComponent<Position_data>(entity)) return false;
264 if (!World::Get().HasComponent<Position_data>(target)) return false;
265
268
269 // Check distance
270 Vector toTarget = targetPos.position - entityPos.position;
271 float distance = toTarget.Magnitude();
272
273 if (distance > visionRadius)
274 return false;
275
276 // TODO: Check angle when entity has a facing direction
277 // For now, assume omnidirectional vision (360 degrees)
278
279 // TODO: Add line-of-sight raycasting when collision system is available
280
281 return true;
282}
283
284// --- AIStateTransitionSystem Implementation ---
285
292
294{
295 // Early return if no entities
296 if (m_entities.empty())
297 return;
298
299 for (EntityID entity : m_entities)
300 {
301 try
302 {
303 // Always sync AIMode to blackboard first (ensures initial sync)
306 blackboard.AIMode = static_cast<int>(state.currentMode);
307
308 UpdateAIState(entity);
309 }
310 catch (const std::exception& e)
311 {
312 SYSTEM_LOG << "AIStateTransitionSystem Error for Entity " << entity << ": " << e.what() << "\n";
313 }
314 }
315}
316
318{
321
323
324 AIMode newMode = state.currentMode;
325
326 // Check for flee condition (low health)
327 if (World::Get().HasComponent<Health_data>(entity))
328 {
330 float healthPercent = static_cast<float>(health.currentHealth) / static_cast<float>(health.maxHealth);
331
333 {
335 }
336 else if (healthPercent <= 0.0f)
337 {
339 }
340 }
341
342 // State machine logic (only if not fleeing or dead)
344 {
345 switch (state.currentMode)
346 {
347 case AIMode::Idle:
348 if (blackboard.hasTarget)
349 {
351 }
352 else if (blackboard.heardNoise)
353 {
355 }
356 else if (blackboard.patrolPointCount > 0)
357 {
359 }
360 break;
361
362 case AIMode::Patrol:
363 if (blackboard.hasTarget)
364 {
366 }
367 else if (blackboard.heardNoise)
368 {
370 }
371 break;
372
373 case AIMode::Combat:
374 if (!blackboard.hasTarget)
375 {
376 // Lost target
377 if (blackboard.timeSinceTargetSeen > 2.0f)
378 {
380 }
381 }
382 break;
383
385 if (blackboard.hasTarget)
386 {
388 }
389 else if (state.timeInCurrentMode > state.investigateTimeout)
390 {
391 // Investigation timeout - return to patrol or idle
392 newMode = (blackboard.patrolPointCount > 0) ? AIMode::Patrol : AIMode::Idle;
393 }
394 break;
395
396 case AIMode::Flee:
397 // Can transition out of flee if health recovers
399 {
401 float healthPercent = static_cast<float>(health.currentHealth) / static_cast<float>(health.maxHealth);
402
403 if (healthPercent > state.fleeHealthThreshold + 0.2f)
404 {
406 }
407 }
408 break;
409
410 case AIMode::Dead:
411 // No transitions from dead state
412 break;
413 }
414 }
415
416 // Apply state change
417 if (newMode != state.currentMode)
418 {
419 state.previousMode = state.currentMode;
420 state.currentMode = newMode;
421 state.timeInCurrentMode = 0.0f;
422
423 // IMPORTANT: Restart tree execution when mode changes
424 // The unified BT will handle mode-specific behavior via CheckBlackboardValue conditions
426 {
428
429 // Handle Dead state - disable tree execution
430 if (newMode == AIMode::Dead)
431 {
432 btRuntime.isActive = false;
433 }
434
435 // DO NOT change AITreeAssetId here! It's set once from the prefab.
436 // The unified BT handles all modes internally via condition checks.
437 btRuntime.needsRestart = true;
438 }
439 }
440 // Note: AIMode sync to blackboard is now done at start of Process() for all entities
441}
442
443// --- BehaviorTreeSystem Implementation ---
444
451
453{
454 // Early return if no entities
455 if (m_entities.empty())
456 return;
457
459
460 for (EntityID entity : m_entities)
461 {
462 try
463 {
467
468 if (!btRuntime.isActive)
469 continue;
470
471 // Get think frequency from AISenses if available
472 float thinkHz = 10.0f; // Default
474 {
476 thinkHz = senses.thinkHz;
477
478 // Timeslicing: only update BT at specified Hz
479 if (currentTime < senses.nextThinkTime)
480 continue;
481
482 World::Get().GetComponent<AISenses_data>(entity).nextThinkTime = currentTime + (1.0f / thinkHz);
483 }
484
485 // Get the behavior tree asset
486 const BehaviorTreeAsset* tree = nullptr;
487
488 if (!btRuntime.AITreePath.empty())
489 {
490 // Load by path (preferred method)
492
493 if (!tree)
494 {
495 std::cerr << "[BehaviorTreeSystem] WARNING: Tree not found: "
496 << btRuntime.AITreePath << " for entity " << identity.name << std::endl;
497 }
498 }
499 else if (btRuntime.AITreeAssetId != 0)
500 {
501 // Fallback to ID lookup
503 }
504
505 if (!tree)
506 {
507 // Tree not found, skip this entity
508 continue;
509 }
510
511 // Phase 38b: Tick OnEvent root nodes (before main Root node execution)
512 // This allows event-driven OnEvent roots to execute in parallel with main tree
514
515 // Restart tree if needed
516 if (btRuntime.needsRestart)
517 {
518 btRuntime.AICurrentNodeIndex = tree->rootNodeId;
519 btRuntime.needsRestart = false;
520 }
521
522 // Get the current node
523 const BTNode* node = tree->GetNode(btRuntime.AICurrentNodeIndex);
524 if (!node)
525 {
526 // Invalid node - restart from root
527 btRuntime.AICurrentNodeIndex = tree->rootNodeId;
528 node = tree->GetNode(btRuntime.AICurrentNodeIndex);
529 }
530
531 if (node)
532 {
533 // Execute the node
534 BTStatus status = ExecuteBTNode(*node, entity, blackboard, *tree);
535 btRuntime.lastStatus = static_cast<uint8_t>(status);
536
537 // Notify debugger if active
539 {
540 g_btDebugWindow->AddExecutionEntry(entity, node->id, node->name, status);
541 }
542
543 // Debug logging (every 2 seconds to avoid spam)
544 static float lastLogTime = 0.0f;
545 if (currentTime - lastLogTime > 2.0f)
546 {
547 if (World::Get().HasComponent<AIState_data>(entity))
548 {
549 const AIState_data& state = World::Get().GetComponent<AIState_data>(entity);
550 const char* modeName = "Unknown";
551 switch (state.currentMode)
552 {
553 case AIMode::Idle: modeName = "Idle"; break;
554 case AIMode::Patrol: modeName = "Patrol"; break;
555 case AIMode::Combat: modeName = "Combat"; break;
556 case AIMode::Flee: modeName = "Flee"; break;
557 case AIMode::Investigate: modeName = "Investigate"; break;
558 case AIMode::Dead: modeName = "Dead"; break;
559 }
560
561 const char* statusName = "Unknown";
562 switch (status)
563 {
564 case BTStatus::Running: statusName = "Running"; break;
565 case BTStatus::Success: statusName = "Success"; break;
566 case BTStatus::Failure: statusName = "Failure"; break;
567 }
568
569 SYSTEM_LOG << "BT[Entity " << entity << "]: Mode=" << modeName
570 << ", Tree=" << btRuntime.AITreeAssetId
571 << ", Node=" << node->name
572 << ", Status=" << statusName;
573
574 if (blackboard.hasTarget)
575 SYSTEM_LOG << ", Target=" << blackboard.targetEntity
576 << ", Dist=" << blackboard.distanceToTarget;
577
578 SYSTEM_LOG << "\n";
579 }
581 }
582
583 // If node completed (success or failure), restart tree next frame
584 if (status != BTStatus::Running)
585 {
586 btRuntime.needsRestart = true;
587 }
588 }
589 }
590 catch (const std::exception& e)
591 {
592 SYSTEM_LOG << "BehaviorTreeSystem Error for Entity " << entity << ": " << e.what() << "\n";
593 }
594 }
595}
596
597// --- AIMotionSystem Implementation ---
598
606
608{
609 // Early return if no entities
610 if (m_entities.empty())
611 return;
612
613 for (EntityID entity : m_entities)
614 {
615 try
616 {
620
621 if (!intent.hasIntent)
622 {
623 // No intent - stop moving
624 movement.direction = Vector(0.0f, 0.0f, 0.0f);
625 movement.velocity = Vector(0.0f, 0.0f, 0.0f);
626 continue;
627 }
628
629 // Calculate direction to target
630 Vector toTarget = intent.targetPosition - pos.position;
631 float distance = toTarget.Magnitude();
632
633 if (distance < 1.f)
634 {
635 // Already at target
636 movement.direction = Vector(0.0f, 0.0f, 0.0f);
637 movement.velocity = Vector(0.0f, 0.0f, 0.0f);
638
639
640 continue;
641 }
642
643 // Normalize direction
644 Vector direction = toTarget * (1.0f / distance);
645
646 // Get speed from PhysicsBody if available
647 float speed = 100.0f; // Default speed
649 {
651 speed = physics.speed;
652 }
653
654 // Apply desired speed multiplier
655 speed *= intent.desiredSpeed;
656
657 // Set movement direction and velocity
658 movement.direction = direction;
659 movement.velocity = direction * speed;
660 }
661 catch (const std::exception& e)
662 {
663 SYSTEM_LOG << "AIMotionSystem Error for Entity " << entity << ": " << e.what() << "\n";
664 }
665 }
666}
667
669{
670 // Optional: render debug info for AI motion (e.g., target positions)
671
672}
Runtime debugger for behavior tree visualization and inspection.
BTStatus ExecuteBTNode(const BTNode &node, EntityID entity, AIBlackboard_data &blackboard, const BehaviorTreeAsset &tree)
void TickEventRoots(EventQueue &eventQueue, const BehaviorTreeAsset &tree, EntityID entity, AIBlackboard_data &blackboard)
Data-driven behavior tree system for AI decision making.
BTStatus
Behavior tree node execution status.
@ Success
Node completed successfully.
@ Running
Node is currently executing.
@ Failure
Node failed.
Olympe::BehaviorTreeDebugWindow * g_btDebugWindow
Core ECS component definitions.
AIMode
AI behavior modes for NPCs.
@ Investigate
ComponentTypeID GetComponentTypeID_Static()
Definition ECS_Entity.h:56
std::uint64_t EntityID
Definition ECS_Entity.h:21
const EntityID INVALID_ENTITY_ID
Definition ECS_Entity.h:23
Olympe::BehaviorTreeDebugWindow * g_btDebugWindow
Core game engine class.
World and ECS Manager for Olympe Engine.
virtual void Process() override
virtual void RenderDebug() override
virtual void Process() override
bool IsTargetVisible(EntityID entity, EntityID target, float visionRadius, float visionAngle)
void UpdateAIState(EntityID entity)
virtual void Process() override
virtual void Process() override
static BehaviorTreeManager & Get()
const BehaviorTreeAsset * GetTreeByPath(const std::string &treePath) const
const BehaviorTreeAsset * GetTreeByAnyId(uint32_t treeId) const
virtual void Process() override
std::set< EntityID > m_entities
Definition ECS_Systems.h:42
ComponentSignature requiredSignature
Definition ECS_Systems.h:39
static EventQueue & Get()
Definition EventQueue.h:34
static float fDt
Delta time between frames in seconds.
Definition GameEngine.h:120
Main debug window for behavior tree runtime visualization.
void AddExecutionEntry(EntityID entity, uint32_t nodeId, const std::string &nodeName, BTStatus status)
Add an execution log entry.
bool IsVisible() const
Check if window is visible.
float Magnitude() const
Definition vector.h:79
static World & Get()
Get singleton instance (short form)
Definition World.h:232
bool HasComponent(EntityID entity) const
Definition World.h:451
bool IsEntityValid(EntityID entity) const
Definition World.h:376
T & GetComponent(EntityID entity)
Definition World.h:438
std::unordered_map< EntityID, ComponentSignature > m_entitySignatures
Definition World.h:636
Represents a single node in a behavior tree.
Identity component for entity identification.
Position component for spatial location.
Vector position
2D/3D position vector
@ Olympe_EventType_Game_TakeScreenshot
#define SYSTEM_LOG