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 // Restart tree if needed
512 if (btRuntime.needsRestart)
513 {
514 btRuntime.AICurrentNodeIndex = tree->rootNodeId;
515 btRuntime.needsRestart = false;
516 }
517
518 // Get the current node
519 const BTNode* node = tree->GetNode(btRuntime.AICurrentNodeIndex);
520 if (!node)
521 {
522 // Invalid node - restart from root
523 btRuntime.AICurrentNodeIndex = tree->rootNodeId;
524 node = tree->GetNode(btRuntime.AICurrentNodeIndex);
525 }
526
527 if (node)
528 {
529 // Execute the node
530 BTStatus status = ExecuteBTNode(*node, entity, blackboard, *tree);
531 btRuntime.lastStatus = static_cast<uint8_t>(status);
532
533 // Notify debugger if active
535 {
536 g_btDebugWindow->AddExecutionEntry(entity, node->id, node->name, status);
537 }
538
539 // Debug logging (every 2 seconds to avoid spam)
540 static float lastLogTime = 0.0f;
541 if (currentTime - lastLogTime > 2.0f)
542 {
543 if (World::Get().HasComponent<AIState_data>(entity))
544 {
545 const AIState_data& state = World::Get().GetComponent<AIState_data>(entity);
546 const char* modeName = "Unknown";
547 switch (state.currentMode)
548 {
549 case AIMode::Idle: modeName = "Idle"; break;
550 case AIMode::Patrol: modeName = "Patrol"; break;
551 case AIMode::Combat: modeName = "Combat"; break;
552 case AIMode::Flee: modeName = "Flee"; break;
553 case AIMode::Investigate: modeName = "Investigate"; break;
554 case AIMode::Dead: modeName = "Dead"; break;
555 }
556
557 const char* statusName = "Unknown";
558 switch (status)
559 {
560 case BTStatus::Running: statusName = "Running"; break;
561 case BTStatus::Success: statusName = "Success"; break;
562 case BTStatus::Failure: statusName = "Failure"; break;
563 }
564
565 SYSTEM_LOG << "BT[Entity " << entity << "]: Mode=" << modeName
566 << ", Tree=" << btRuntime.AITreeAssetId
567 << ", Node=" << node->name
568 << ", Status=" << statusName;
569
570 if (blackboard.hasTarget)
571 SYSTEM_LOG << ", Target=" << blackboard.targetEntity
572 << ", Dist=" << blackboard.distanceToTarget;
573
574 SYSTEM_LOG << "\n";
575 }
577 }
578
579 // If node completed (success or failure), restart tree next frame
580 if (status != BTStatus::Running)
581 {
582 btRuntime.needsRestart = true;
583 }
584 }
585 }
586 catch (const std::exception& e)
587 {
588 SYSTEM_LOG << "BehaviorTreeSystem Error for Entity " << entity << ": " << e.what() << "\n";
589 }
590 }
591}
592
593// --- AIMotionSystem Implementation ---
594
602
604{
605 // Early return if no entities
606 if (m_entities.empty())
607 return;
608
609 for (EntityID entity : m_entities)
610 {
611 try
612 {
616
617 if (!intent.hasIntent)
618 {
619 // No intent - stop moving
620 movement.direction = Vector(0.0f, 0.0f, 0.0f);
621 movement.velocity = Vector(0.0f, 0.0f, 0.0f);
622 continue;
623 }
624
625 // Calculate direction to target
626 Vector toTarget = intent.targetPosition - pos.position;
627 float distance = toTarget.Magnitude();
628
629 if (distance < 1.f)
630 {
631 // Already at target
632 movement.direction = Vector(0.0f, 0.0f, 0.0f);
633 movement.velocity = Vector(0.0f, 0.0f, 0.0f);
634
635
636 continue;
637 }
638
639 // Normalize direction
640 Vector direction = toTarget * (1.0f / distance);
641
642 // Get speed from PhysicsBody if available
643 float speed = 100.0f; // Default speed
645 {
647 speed = physics.speed;
648 }
649
650 // Apply desired speed multiplier
651 speed *= intent.desiredSpeed;
652
653 // Set movement direction and velocity
654 movement.direction = direction;
655 movement.velocity = direction * speed;
656 }
657 catch (const std::exception& e)
658 {
659 SYSTEM_LOG << "AIMotionSystem Error for Entity " << entity << ": " << e.what() << "\n";
660 }
661 }
662}
663
665{
666 // Optional: render debug info for AI motion (e.g., target positions)
667
668}
Runtime debugger for behavior tree visualization and inspection.
BTStatus ExecuteBTNode(const BTNode &node, EntityID entity, AIBlackboard_data &blackboard, const BehaviorTreeAsset &tree)
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.
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:74
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