Olympe Engine 2.0
2D Game Engine with ECS Architecture
Loading...
Searching...
No Matches
BehaviorTree.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
8Behavior Tree implementation: JSON loading and built-in node execution.
9
10*/
11
12#include "BehaviorTree.h"
14#include "../ECS_Components_AI.h"
15#include "../ECS_Components.h"
16#include "../World.h"
17#include "../system/system_utils.h"
18#include "../json_helper.h"
19#include "../CollisionMap.h"
20#include "../GameEngine.h"
21#include <cmath>
22#include <functional>
23#include <set>
24
26
27// --- BehaviorTreeManager Implementation ---
28
29bool BehaviorTreeManager::LoadTreeFromFile(const std::string& filepath, uint32_t treeId)
30{
31 std::cout << "\n[BehaviorTreeManager] ========================================" << std::endl;
32 std::cout << "[BehaviorTreeManager] Loading: " << filepath << std::endl;
33
34 try
35 {
36 // 1. Load JSON file
37 std::cout << "[BehaviorTreeManager] Step 1: Loading JSON file..." << std::endl;
38 json j;
39 if (!JsonHelper::LoadJsonFromFile(filepath, j))
40 {
41 std::cerr << "[BehaviorTreeManager] ERROR: Failed to load file: " << filepath << std::endl;
42 std::cout << "[BehaviorTreeManager] ========================================+n" << std::endl;
43 return false;
44 }
45 std::cout << "[BehaviorTreeManager] JSON loaded successfully" << std::endl;
46
47 // 2. Detect version
48 std::cout << "[BehaviorTreeManager] Step 2: Detecting format version..." << std::endl;
49 bool isV2 = j.contains("schema_version") && j["schema_version"].get<int>() == 2;
50 std::cout << "[BehaviorTreeManager] Version: " << (isV2 ? "v2" : "v1") << std::endl;
51
52 // 3. Extract tree metadata and data section
53 std::cout << "[BehaviorTreeManager] Step 3: Extracting tree metadata..." << std::endl;
55 tree.id = treeId;
56
57 const json* dataSection = &j;
58
59 if (isV2)
60 {
61 tree.name = JsonHelper::GetString(j, "name", "Unnamed Tree");
62
63 if (!j.contains("data"))
64 {
65 std::cerr << "[BehaviorTreeManager] ERROR: v2 format but no 'data' section" << std::endl;
66 std::cout << "[BehaviorTreeManager] ========================================+n" << std::endl;
67 return false;
68 }
69
70 dataSection = &j["data"];
71 std::cout << "[BehaviorTreeManager] Extracted 'data' section from v2 format" << std::endl;
72 }
73 else
74 {
75 tree.name = JsonHelper::GetString(j, "name", "Unnamed Tree");
76 std::cout << "[BehaviorTreeManager] Using root as data section (v1 format)" << std::endl;
77 }
78
79 tree.rootNodeId = JsonHelper::GetUInt(*dataSection, "rootNodeId", 0);
80 std::cout << "[BehaviorTreeManager] Tree name: " << tree.name << std::endl;
81 std::cout << "[BehaviorTreeManager] Root node ID: " << tree.rootNodeId << std::endl;
82
83 // 4. Parse nodes
84 std::cout << "[BehaviorTreeManager] Step 4: Parsing nodes..." << std::endl;
85 if (!JsonHelper::IsArray(*dataSection, "nodes"))
86 {
87 std::cerr << "[BehaviorTreeManager] ERROR: No 'nodes' array in data section" << std::endl;
88 std::cerr << "[BehaviorTreeManager] This may be an empty or invalid tree" << std::endl;
89
90 // For debug: display JSON structure
91 std::cout << "[BehaviorTreeManager] JSON structure:" << std::endl;
92 std::cout << j.dump(2) << std::endl;
93
94 std::cout << "[BehaviorTreeManager] ========================================+n" << std::endl;
95 return false;
96 }
97
98 // Count nodes first
99 int nodeCount = 0;
100 JsonHelper::ForEachInArray(*dataSection, "nodes", [&nodeCount](const json& nodeJson, size_t i) { nodeCount++; });
101 std::cout << "[BehaviorTreeManager] Found " << nodeCount << " nodes to parse" << std::endl;
102
103 JsonHelper::ForEachInArray(*dataSection, "nodes", [&tree, isV2](const json& nodeJson, size_t i)
104 {
105 BTNode node;
107 node.name = JsonHelper::GetString(nodeJson, "name", "");
108
109 std::string typeStr = JsonHelper::GetString(nodeJson, "type", "Action");
110 if (typeStr == "Selector") node.type = BTNodeType::Selector;
111 else if (typeStr == "Sequence") node.type = BTNodeType::Sequence;
112 else if (typeStr == "Condition") node.type = BTNodeType::Condition;
113 else if (typeStr == "Action") node.type = BTNodeType::Action;
114 else if (typeStr == "Inverter") node.type = BTNodeType::Inverter;
115 else if (typeStr == "Repeater") node.type = BTNodeType::Repeater;
116
117 // Parse child IDs for composite nodes
118 if (JsonHelper::IsArray(nodeJson, "children"))
119 {
120 JsonHelper::ForEachInArray(nodeJson, "children", [&node](const json& childId, size_t j)
121 {
122 node.childIds.push_back(childId.get<uint32_t>());
123 });
124 }
125
126 // Parse condition type
127 if (node.type == BTNodeType::Condition && nodeJson.contains("conditionType"))
128 {
129 std::string condStr = JsonHelper::GetString(nodeJson, "conditionType", "");
130 node.conditionTypeString = condStr; // Store string for flexible conditions
131
132 if (condStr == "TargetVisible" || condStr == "HasTarget")
133 node.conditionType = BTConditionType::TargetVisible;
134 else if (condStr == "TargetInRange" || condStr == "IsTargetInAttackRange")
135 node.conditionType = BTConditionType::TargetInRange;
136 else if (condStr == "HealthBelow")
137 node.conditionType = BTConditionType::HealthBelow;
138 else if (condStr == "HasMoveGoal")
139 node.conditionType = BTConditionType::HasMoveGoal;
140 else if (condStr == "CanAttack")
141 node.conditionType = BTConditionType::CanAttack;
142 else if (condStr == "HeardNoise")
143 node.conditionType = BTConditionType::HeardNoise;
144 // NEW: Wander behavior conditions
145 else if (condStr == "IsWaitTimerExpired")
147 else if (condStr == "HasNavigableDestination")
149 else if (condStr == "HasValidPath")
150 node.conditionType = BTConditionType::HasValidPath;
151 else if (condStr == "HasReachedDestination")
153 // NEW: CheckBlackboardValue is handled via string type, not enum
154
155 // Handle v2 format parameters (nested in "parameters" object)
156 if (isV2 && nodeJson.contains("parameters") && nodeJson["parameters"].is_object())
157 {
158 const json& params = nodeJson["parameters"];
159 node.conditionParam = JsonHelper::GetFloat(params, "param", 0.0f);
160
161 // Parse flexible parameters for conditions like CheckBlackboardValue
162 for (auto it = params.begin(); it != params.end(); ++it)
163 {
164 if (it.value().is_string())
165 node.stringParams[it.key()] = it.value().get<std::string>();
166 else if (it.value().is_number_integer())
167 node.intParams[it.key()] = it.value().get<int>();
168 else if (it.value().is_number_float())
169 node.floatParams[it.key()] = it.value().get<float>();
170 }
171 }
172 else
173 {
174 // v1 format (flat)
175 node.conditionParam = JsonHelper::GetFloat(nodeJson, "param", 0.0f);
176 }
177 }
178
179 // Parse action type
180 if (node.type == BTNodeType::Action && nodeJson.contains("actionType"))
181 {
182 std::string actStr = JsonHelper::GetString(nodeJson, "actionType", "");
183 if (actStr == "SetMoveGoalToLastKnownTargetPos")
185 else if (actStr == "SetMoveGoalToTarget")
187 else if (actStr == "SetMoveGoalToPatrolPoint")
189 else if (actStr == "MoveToGoal" || actStr == "MoveTo")
190 node.actionType = BTActionType::MoveToGoal;
191 else if (actStr == "AttackIfClose" || actStr == "AttackMelee")
193 else if (actStr == "PatrolPickNextPoint")
195 else if (actStr == "ClearTarget")
196 node.actionType = BTActionType::ClearTarget;
197 else if (actStr == "Idle")
198 node.actionType = BTActionType::Idle;
199 // NEW: Wander behavior actions
200 else if (actStr == "WaitRandomTime")
202 else if (actStr == "ChooseRandomNavigablePoint")
204 else if (actStr == "RequestPathfinding")
206 else if (actStr == "FollowPath")
207 node.actionType = BTActionType::FollowPath;
208
209 // Handle v2 format parameters (nested in "parameters" object)
210 if (isV2 && nodeJson.contains("parameters") && nodeJson["parameters"].is_object())
211 {
212 const json& params = nodeJson["parameters"];
213 node.actionParam1 = JsonHelper::GetFloat(params, "param1", 0.0f);
214 node.actionParam2 = JsonHelper::GetFloat(params, "param2", 0.0f);
215 }
216 else
217 {
218 // v1 format (flat)
219 node.actionParam1 = JsonHelper::GetFloat(nodeJson, "param1", 0.0f);
220 node.actionParam2 = JsonHelper::GetFloat(nodeJson, "param2", 0.0f);
221 }
222 }
223
224 // Parse decorator child
225 if (node.type == BTNodeType::Inverter || node.type == BTNodeType::Repeater)
226 {
227 node.decoratorChildId = JsonHelper::GetInt(nodeJson, "decoratorChildId", 0);
228 }
229
230 if (node.type == BTNodeType::Repeater)
231 {
232 node.repeatCount = JsonHelper::GetInt(nodeJson, "repeatCount", 0);
233 }
234
235 tree.nodes.push_back(node);
236
237 std::cout << "[BehaviorTreeManager] Node " << node.id << ": " << node.name
238 << " (" << typeStr << ") children: " << node.childIds.size() << std::endl;
239 });
240
241 std::cout << "[BehaviorTreeManager] Parsed " << tree.nodes.size() << " nodes successfully" << std::endl;
242
243 // 5. Validate tree structure
244 std::cout << "[BehaviorTreeManager] Step 5: Validating tree structure..." << std::endl;
245 std::string validationError;
247 if (!valid)
248 {
249 std::cerr << "[BehaviorTreeManager] WARNING: Tree validation failed: " << validationError << std::endl;
250 // Don't fail loading - allow hot-reload to fix issues
251 }
252 else
253 {
254 std::cout << "[BehaviorTreeManager] Tree validation: OK" << std::endl;
255 }
256
257 // 6. Store the tree
258 std::cout << "[BehaviorTreeManager] Step 6: Registering tree..." << std::endl;
259 m_trees.push_back(tree);
260
261 // Register the path -> ID mapping
262 m_pathToIdMap[filepath] = treeId;
263
264 std::cout << "[BehaviorTreeManager] SUCCESS: Loaded '" << tree.name << "' (ID=" << treeId << ") with "
265 << tree.nodes.size() << " nodes" << std::endl;
266 std::cout << "[BehaviorTreeManager] Registered path mapping: " << filepath << " -> ID " << treeId << "\n";
267 std::cout << "[BehaviorTreeManager] ========================================+n" << std::endl;
268
269 return true;
270 }
271 catch (const std::exception& e)
272 {
273 std::cerr << "\n[BehaviorTreeManager] !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" << std::endl;
274 std::cerr << "[BehaviorTreeManager] EXCEPTION: " << e.what() << std::endl;
275 std::cerr << "[BehaviorTreeManager] !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" << std::endl;
276 std::cout << "[BehaviorTreeManager] ========================================+n" << std::endl;
277 return false;
278 }
279}
280
282{
283 for (const auto& tree : m_trees)
284 {
285 if (tree.id == treeId)
286 return &tree;
287 }
288 return nullptr;
289}
290
292{
293 m_trees.clear();
294 m_pathToIdMap.clear();
295}
296
298{
299 // Find the tree
300 for (auto& tree : m_trees)
301 {
302 if (tree.id == treeId)
303 {
304 // Get the original filepath (we need to store it)
305 // For now, reconstruct it from tree name
306 std::string filepath = "Blueprints/AI/" + tree.name + ".json";
307
308 // Remove old tree
309 m_trees.erase(std::remove_if(m_trees.begin(), m_trees.end(),
310 [treeId](const BehaviorTreeAsset& t) { return t.id == treeId; }),
311 m_trees.end());
312
313 // Load new version
314 bool success = LoadTreeFromFile(filepath, treeId);
315 if (success)
316 {
317 SYSTEM_LOG << "BehaviorTreeManager: Hot-reloaded tree ID=" << treeId << "\n";
318 }
319 return success;
320 }
321 }
322
323 SYSTEM_LOG << "BehaviorTreeManager: Cannot reload tree ID=" << treeId << " (not found)\n";
324 return false;
325}
326
327bool BehaviorTreeManager::ValidateTree(const BehaviorTreeAsset& tree, std::string& errorMessage) const
328{
329 errorMessage.clear();
330
331 // Check if tree has nodes
332 if (tree.nodes.empty())
333 {
334 errorMessage = "Tree has no nodes";
335 return false;
336 }
337
338 // Check if root node exists
339 const BTNode* root = tree.GetNode(tree.rootNodeId);
340 if (!root)
341 {
342 errorMessage = "Root node ID " + std::to_string(tree.rootNodeId) + " not found";
343 return false;
344 }
345
346 // Validate each node
347 for (const auto& node : tree.nodes)
348 {
349 // Check composite nodes have children
350 if (node.type == BTNodeType::Selector || node.type == BTNodeType::Sequence)
351 {
352 if (node.childIds.empty())
353 {
354 errorMessage = "Composite node '" + node.name + "' (ID=" + std::to_string(node.id) + ") has no children";
355 return false;
356 }
357
358 // Validate all children exist
359 for (uint32_t childId : node.childIds)
360 {
361 if (!tree.GetNode(childId))
362 {
363 errorMessage = "Node '" + node.name + "' references missing child ID " + std::to_string(childId);
364 return false;
365 }
366 }
367 }
368
369 // Check decorator nodes have a child
370 if (node.type == BTNodeType::Inverter || node.type == BTNodeType::Repeater)
371 {
372 if (!tree.GetNode(node.decoratorChildId))
373 {
374 errorMessage = "Decorator node '" + node.name + "' references missing child ID " + std::to_string(node.decoratorChildId);
375 return false;
376 }
377 }
378
379 // Check for duplicate node IDs
380 int count = 0;
381 for (const auto& other : tree.nodes)
382 {
383 if (other.id == node.id)
384 count++;
385 }
386 if (count > 1)
387 {
388 errorMessage = "Duplicate node ID " + std::to_string(node.id);
389 return false;
390 }
391 }
392
393 return true;
394}
395
396// --- Behavior Tree Execution ---
397
399{
400 switch (node.type)
401 {
403 {
404 // OR node: succeeds if any child succeeds
405 for (uint32_t childId : node.childIds)
406 {
407 const BTNode* child = tree.GetNode(childId);
408 if (!child) continue;
409
410 BTStatus status = ExecuteBTNode(*child, entity, blackboard, tree);
411 if (status == BTStatus::Success)
412 return BTStatus::Success;
413 if (status == BTStatus::Running)
414 return BTStatus::Running;
415 }
416 return BTStatus::Failure;
417 }
418
420 {
421 // AND node: succeeds if all children succeed
422 for (uint32_t childId : node.childIds)
423 {
424 const BTNode* child = tree.GetNode(childId);
425 if (!child) continue;
426
427 BTStatus status = ExecuteBTNode(*child, entity, blackboard, tree);
428 if (status == BTStatus::Failure)
429 return BTStatus::Failure;
430 if (status == BTStatus::Running)
431 return BTStatus::Running;
432 }
433 return BTStatus::Success;
434 }
435
437 {
438 // Check if this is a string-based condition (like CheckBlackboardValue)
439 if (!node.conditionTypeString.empty() && node.conditionTypeString == "CheckBlackboardValue")
440 {
441 // Execute CheckBlackboardValue condition
442 std::string key = node.GetParameterString("key");
443 std::string op = node.GetParameterString("operator");
444 int expectedValue = node.GetParameterInt("value");
445
446 int actualValue = 0;
447
448 // Get value from blackboard
449 if (key == "AIMode")
450 {
451 actualValue = blackboard.AIMode;
452 }
453 else
454 {
455 // Future: support other blackboard integer fields
456 return BTStatus::Failure; // Key not found
457 }
458
459 // Perform comparison
460 if (op == "Equals" || op == "equals" || op == "==")
462 else if (op == "NotEquals" || op == "notequals" || op == "!=")
464 else if (op == "GreaterThan" || op == "greaterthan" || op == ">")
466 else if (op == "LessThan" || op == "lessthan" || op == "<")
468 else if (op == "GreaterOrEqual" || op == "greaterorequal" || op == ">=")
470 else if (op == "LessOrEqual" || op == "lessorequal" || op == "<=")
472
473 return BTStatus::Failure;
474 }
475 else
476 {
477 // Execute enum-based condition (legacy)
478 return ExecuteBTCondition(node.conditionType, node.conditionParam, entity, blackboard);
479 }
480 }
481
483 {
484 return ExecuteBTAction(node.actionType, node.actionParam1, node.actionParam2, entity, blackboard);
485 }
486
488 {
489 const BTNode* child = tree.GetNode(node.decoratorChildId);
490 if (!child) return BTStatus::Failure;
491
492 BTStatus status = ExecuteBTNode(*child, entity, blackboard, tree);
493 if (status == BTStatus::Success)
494 return BTStatus::Failure;
495 if (status == BTStatus::Failure)
496 return BTStatus::Success;
497 return status;
498 }
499
501 {
502 // Simplified repeater: just execute once per tick
503 const BTNode* child = tree.GetNode(node.decoratorChildId);
504 if (!child) return BTStatus::Failure;
505
506 return ExecuteBTNode(*child, entity, blackboard, tree);
507 }
508 }
509
510 return BTStatus::Failure;
511}
512
514{
515 switch (condType)
516 {
518 return blackboard.targetVisible ? BTStatus::Success : BTStatus::Failure;
519
521 if (!blackboard.hasTarget) return BTStatus::Failure;
522 return (blackboard.distanceToTarget <= param) ? BTStatus::Success : BTStatus::Failure;
523
526 {
528 float healthPercent = static_cast<float>(health.currentHealth) / static_cast<float>(health.maxHealth);
530 }
531 return BTStatus::Failure;
532
534 return blackboard.hasMoveGoal ? BTStatus::Success : BTStatus::Failure;
535
538
540 return blackboard.heardNoise ? BTStatus::Success : BTStatus::Failure;
541
542 // NEW: Wander behavior conditions
544 return (blackboard.wanderWaitTimer >= blackboard.wanderTargetWaitTime) ? BTStatus::Success : BTStatus::Failure;
545
547 return blackboard.hasWanderDestination ? BTStatus::Success : BTStatus::Failure;
548
550 // Check if NavigationAgent has a valid path
552 {
554 return (!navAgent.currentPath.empty()) ? BTStatus::Success : BTStatus::Failure;
555 }
556 return BTStatus::Failure;
557
559 if (!blackboard.hasWanderDestination) return BTStatus::Failure;
560
562 {
564 Vector vDest = pos.position;
565 vDest -= blackboard.wanderDestination;
566 float dist = vDest.Magnitude();
567
568 // Use arrival threshold from MoveIntent if available, otherwise use default
569 float threshold = 5.0f;
571 {
574 }
575
577 }
578 return BTStatus::Failure;
579 }
580
581 return BTStatus::Failure;
582}
583
584BTStatus ExecuteBTAction(BTActionType actionType, float param1, float param2, EntityID entity, AIBlackboard_data& blackboard)
585{
586 switch (actionType)
587 {
589 blackboard.moveGoal = blackboard.lastKnownTargetPosition;
590 blackboard.hasMoveGoal = true;
591 return BTStatus::Success;
592
594 if (blackboard.hasTarget && blackboard.targetEntity != INVALID_ENTITY_ID)
595 {
597 {
599 blackboard.moveGoal = targetPos.position;
600 blackboard.hasMoveGoal = true;
601 return BTStatus::Success;
602 }
603 }
604 return BTStatus::Failure;
605
607 if (blackboard.patrolPointCount > 0)
608 {
609 int index = static_cast<int>(param1);
610 if (index < 0 || index >= blackboard.patrolPointCount)
611 index = blackboard.currentPatrolPoint;
612
613 blackboard.moveGoal = blackboard.patrolPoints[index];
614 blackboard.hasMoveGoal = true;
615 return BTStatus::Success;
616 }
617 return BTStatus::Failure;
618
620 if (!blackboard.hasMoveGoal) return BTStatus::Failure;
621
622 // Set MoveIntent if component exists
624 {
627 intent.desiredSpeed = (param1 > 0.0f) ? param1 : 1.0f;
628 intent.hasIntent = true;
629
630 // Check if we've arrived
632 {
634 float dist = (pos.position - blackboard.moveGoal).Magnitude();
635 if (dist < intent.arrivalThreshold)
636 {
637 blackboard.hasMoveGoal = false;
638 intent.hasIntent = false;
639 return BTStatus::Success;
640 }
641 }
642
643 return BTStatus::Running;
644 }
645 return BTStatus::Failure;
646
648 {
649 float range = (param1 > 0.0f) ? param1 : 50.0f;
650 if (blackboard.hasTarget && blackboard.distanceToTarget <= range && blackboard.canAttack)
651 {
652 // Set AttackIntent if component exists
654 {
656 intent.targetEntity = blackboard.targetEntity;
657 intent.targetPosition = blackboard.lastKnownTargetPosition;
658 intent.range = range;
659 intent.damage = (param2 > 0.0f) ? param2 : 10.0f;
660 intent.hasIntent = true;
661
662 blackboard.canAttack = false;
663 return BTStatus::Success;
664 }
665 }
666 return BTStatus::Failure;
667 }
668
670 if (blackboard.patrolPointCount > 0)
671 {
672 blackboard.currentPatrolPoint = (blackboard.currentPatrolPoint + 1) % blackboard.patrolPointCount;
673 blackboard.moveGoal = blackboard.patrolPoints[blackboard.currentPatrolPoint];
674 blackboard.hasMoveGoal = true;
675 return BTStatus::Success;
676 }
677 return BTStatus::Failure;
678
680 blackboard.hasTarget = false;
681 blackboard.targetEntity = INVALID_ENTITY_ID;
682 blackboard.targetVisible = false;
683 return BTStatus::Success;
684
686 // Do nothing
687 return BTStatus::Success;
688
689 // NEW: Wander behavior actions
691 {
692 // If timer not initialized, create a random time
693 if (blackboard.wanderTargetWaitTime == 0.0f)
694 {
695 float minWait = (param1 > 0.0f) ? param1 : 2.0f;
696 float maxWait = (param2 > 0.0f) ? param2 : 6.0f;
697
698 // Random generation between min and max
699 float randomFactor = static_cast<float>(rand()) / static_cast<float>(RAND_MAX);
700 blackboard.wanderTargetWaitTime = minWait + randomFactor * (maxWait - minWait);
701 blackboard.wanderWaitTimer = 0.0f;
702 }
703
704 // Increment timer with engine deltaTime
705 blackboard.wanderWaitTimer += GameEngine::fDt;
706
707 // Check if timer expired
708 if (blackboard.wanderWaitTimer >= blackboard.wanderTargetWaitTime)
709 {
710 // Reset for next cycle
711 blackboard.wanderTargetWaitTime = 0.0f;
712 blackboard.wanderWaitTimer = 0.0f;
713 return BTStatus::Success;
714 }
715
716 return BTStatus::Running;
717 }
718
720 {
721 // Get current position
722 if (!World::Get().HasComponent<Position_data>(entity))
723 return BTStatus::Failure;
724
726
727 // Parameters
728 float searchRadius = (param1 > 0.0f) ? param1 : blackboard.wanderSearchRadius;
729 int maxAttempts = (param2 > 0.0f) ? static_cast<int>(param2) : blackboard.wanderMaxSearchAttempts;
730
731 // Search for a random navigable point
732 float destX, destY;
734 pos.position.x, pos.position.y, searchRadius, maxAttempts, destX, destY
735 );
736
737 if (found)
738 {
739 blackboard.wanderDestination = Vector(destX, destY);
740 blackboard.hasWanderDestination = true;
741 blackboard.moveGoal = blackboard.wanderDestination;
742 blackboard.hasMoveGoal = true;
743 return BTStatus::Success;
744 }
745
746 return BTStatus::Failure;
747 }
748
750 {
751 if (!blackboard.hasMoveGoal) return BTStatus::Failure;
752
753 // Go through MoveIntent_data with pathfinding enabled
755 {
758 intent.desiredSpeed = 1.0f;
759 intent.hasIntent = true;
760 intent.usePathfinding = true; // Enable pathfinding
761 intent.avoidObstacles = true;
762
763 return BTStatus::Success;
764 }
765
766 return BTStatus::Failure;
767 }
768
770 {
771 if (!blackboard.hasWanderDestination) return BTStatus::Failure;
772
773 // Check if we have an active MoveIntent
775 {
777
778 // Maintain the active intent
779 if (!intent.hasIntent)
780 {
781 intent.targetPosition = blackboard.wanderDestination;
782 intent.desiredSpeed = 1.0f;
783 intent.hasIntent = true;
784 intent.usePathfinding = true;
785 }
786
787 // Check if we have arrived
789 {
791 Vector vDest = pos.position;
792 vDest -= blackboard.wanderDestination;
793 float dist = vDest.Magnitude();
794
795 if (dist < intent.arrivalThreshold)
796 {
797 // Arrived at destination
798 blackboard.hasWanderDestination = false;
799 blackboard.hasMoveGoal = false;
800 intent.hasIntent = false;
801 return BTStatus::Success;
802 }
803 }
804
805 return BTStatus::Running;
806 }
807
808 return BTStatus::Failure;
809 }
810 }
811
812 return BTStatus::Failure;
813}
814
815// --- Path-to-ID Registry Methods ---
816
817uint32_t BehaviorTreeManager::GetTreeIdFromPath(const std::string& treePath) const
818{
819 auto it = m_pathToIdMap.find(treePath);
820 if (it != m_pathToIdMap.end())
821 return it->second;
822
823 // Fallback: generate ID from path if not in registry
824 // This allows forward compatibility with paths that aren't loaded yet
826}
827
828bool BehaviorTreeManager::IsTreeLoadedByPath(const std::string& treePath) const
829{
830 return m_pathToIdMap.find(treePath) != m_pathToIdMap.end();
831}
832
833const BehaviorTreeAsset* BehaviorTreeManager::GetTreeByPath(const std::string& treePath) const
834{
835 uint32_t treeId = GetTreeIdFromPath(treePath);
836 return GetTree(treeId);
837}
838
840{
841 // Strategy 1: Direct ID lookup
842 for (const auto& tree : m_trees)
843 {
844 if (tree.id == treeId)
845 return &tree;
846 }
847
848 // No additional strategies currently implemented
849 // Future enhancement: Could add tree structure matching for corrupted prefabs
850
851 return nullptr;
852}
853
855{
856 // Check path-to-ID registry
857 for (const auto& entry : m_pathToIdMap)
858 {
859 if (entry.second == treeId)
860 return entry.first;
861 }
862
863 // Check if a tree with this ID exists (might be loaded with different path)
864 for (const auto& tree : m_trees)
865 {
866 if (tree.id == treeId)
867 return "TreeName:" + tree.name; // Prefix to distinguish from actual file path
868 }
869
870 return "";
871}
872
874{
875 std::cout << "[BehaviorTreeManager] Loaded trees (" << m_trees.size() << "):" << std::endl;
876 for (const auto& tree : m_trees)
877 {
878 std::cout << " - ID=" << tree.id << " Name='" << tree.name << "' Nodes=" << tree.nodes.size() << std::endl;
879 }
880
881 std::cout << "[BehaviorTreeManager] Path-to-ID registry (" << m_pathToIdMap.size() << "):" << std::endl;
882 for (const auto& entry : m_pathToIdMap)
883 {
884 std::cout << " - '" << entry.first << "' -> ID=" << entry.second << std::endl;
885 }
886}
887
888// =============================================================================
889// BehaviorTreeAsset - Validation Methods
890// =============================================================================
891
892std::vector<BTValidationMessage> BehaviorTreeAsset::ValidateTreeFull() const
893{
894 std::vector<BTValidationMessage> messages;
895
896 // Rule 1: Exactly one root node
897 int rootCount = 0;
898 const BTNode* rootNode = nullptr;
899 for (const auto& node : nodes)
900 {
901 if (node.id == rootNodeId)
902 {
903 rootNode = &node;
904 rootCount++;
905 }
906 }
907
908 if (rootCount == 0)
909 {
912 msg.nodeId = 0;
913 msg.message = "No root node found (rootNodeId=" + std::to_string(rootNodeId) + ")";
914 messages.push_back(msg);
915 return messages; // Cannot continue without root
916 }
917 else if (rootCount > 1)
918 {
921 msg.nodeId = 0;
922 msg.message = "Multiple nodes with root ID found";
923 messages.push_back(msg);
924 }
925
926 // Build parent count map
927 std::map<uint32_t, int> parentCounts;
928 for (const auto& node : nodes)
929 {
930 parentCounts[node.id] = 0;
931 }
932
933 // Count parents for each node
934 for (const auto& node : nodes)
935 {
936 // Check childIds for composites
937 if (node.type == BTNodeType::Selector || node.type == BTNodeType::Sequence)
938 {
939 for (uint32_t childId : node.childIds)
940 {
941 if (parentCounts.find(childId) != parentCounts.end())
942 {
943 parentCounts[childId]++;
944 }
945 else
946 {
949 msg.nodeId = node.id;
950 msg.message = "Node references non-existent child ID " + std::to_string(childId);
951 messages.push_back(msg);
952 }
953 }
954 }
955
956 // Check decoratorChildId for decorators
957 if (node.type == BTNodeType::Inverter || node.type == BTNodeType::Repeater)
958 {
959 if (node.decoratorChildId != 0)
960 {
961 if (parentCounts.find(node.decoratorChildId) != parentCounts.end())
962 {
963 parentCounts[node.decoratorChildId]++;
964 }
965 else
966 {
969 msg.nodeId = node.id;
970 msg.message = "Decorator references non-existent child ID " + std::to_string(node.decoratorChildId);
971 messages.push_back(msg);
972 }
973 }
974 }
975 }
976
977 // Rule 2: No node should have multiple parents
978 for (const auto& entry : parentCounts)
979 {
980 if (entry.second > 1)
981 {
984 msg.nodeId = entry.first;
985 msg.message = "Node has multiple parents (count=" + std::to_string(entry.second) + ")";
986 messages.push_back(msg);
987 }
988 }
989
990 // Rule 3: Root should have no parent
991 if (rootNode && parentCounts[rootNode->id] > 0)
992 {
995 msg.nodeId = rootNode->id;
996 msg.message = "Root node has parent(s)";
997 messages.push_back(msg);
998 }
999
1000 // Rule 4: No orphan nodes (except root)
1001 for (const auto& entry : parentCounts)
1002 {
1003 if (entry.second == 0 && entry.first != rootNodeId)
1004 {
1007 msg.nodeId = entry.first;
1008 msg.message = "Orphan node (no parent, not root)";
1009 messages.push_back(msg);
1010 }
1011 }
1012
1013 // Rule 5: Validate node type-specific constraints
1014 for (const auto& node : nodes)
1015 {
1016 if (node.type == BTNodeType::Inverter || node.type == BTNodeType::Repeater)
1017 {
1018 // Decorators must have exactly 1 child
1019 if (node.decoratorChildId == 0)
1020 {
1023 msg.nodeId = node.id;
1024 msg.message = "Decorator has no child (decoratorChildId=0)";
1025 messages.push_back(msg);
1026 }
1027 }
1028 else if (node.type == BTNodeType::Selector || node.type == BTNodeType::Sequence)
1029 {
1030 // Composites should have at least 1 child (warning if 0)
1031 if (node.childIds.empty())
1032 {
1035 msg.nodeId = node.id;
1036 msg.message = "Composite has no children";
1037 messages.push_back(msg);
1038 }
1039 }
1040 }
1041
1042 // Rule 6: Detect cycles
1043 for (const auto& node : nodes)
1044 {
1045 if (DetectCycle(node.id))
1046 {
1049 msg.nodeId = node.id;
1050 msg.message = "Cycle detected starting from this node";
1051 messages.push_back(msg);
1052 }
1053 }
1054
1055 return messages;
1056}
1057
1059{
1060 std::set<uint32_t> visited;
1061 std::set<uint32_t> recursionStack;
1062
1063 // DFS helper function
1064 std::function<bool(uint32_t)> dfs = [&](uint32_t nodeId) -> bool
1065 {
1066 if (recursionStack.find(nodeId) != recursionStack.end())
1067 {
1068 return true; // Cycle detected
1069 }
1070
1071 if (visited.find(nodeId) != visited.end())
1072 {
1073 return false; // Already checked this node
1074 }
1075
1076 visited.insert(nodeId);
1077 recursionStack.insert(nodeId);
1078
1079 const BTNode* node = GetNode(nodeId);
1080 if (!node)
1081 {
1082 recursionStack.erase(nodeId);
1083 return false;
1084 }
1085
1086 // Check children in childIds (composites)
1087 for (uint32_t childId : node->childIds)
1088 {
1089 if (dfs(childId))
1090 {
1091 return true;
1092 }
1093 }
1094
1095 // Check decoratorChildId (decorators)
1096 if ((node->type == BTNodeType::Inverter || node->type == BTNodeType::Repeater) &&
1097 node->decoratorChildId != 0)
1098 {
1099 if (dfs(node->decoratorChildId))
1100 {
1101 return true;
1102 }
1103 }
1104
1105 recursionStack.erase(nodeId);
1106 return false;
1107 };
1108
1109 return dfs(startNodeId);
1110}
1111
1112// =============================================================================
1113// BehaviorTreeAsset - Editor CRUD Operations
1114// =============================================================================
1115
1117{
1118 uint32_t maxId = 0;
1119 for (const auto& node : nodes)
1120 {
1121 if (node.id > maxId)
1122 {
1123 maxId = node.id;
1124 }
1125 }
1126 return maxId + 1;
1127}
1128
1129uint32_t BehaviorTreeAsset::AddNode(BTNodeType type, const std::string& name, const Vector& position)
1130{
1132 newNode.type = type;
1134 newNode.name = name;
1135
1136 // Set default parameters based on type
1137 if (type == BTNodeType::Action)
1138 {
1139 newNode.actionType = BTActionType::Idle;
1140 newNode.actionParam1 = 0.0f;
1141 newNode.actionParam2 = 0.0f;
1142 }
1143 else if (type == BTNodeType::Condition)
1144 {
1146 newNode.conditionParam = 0.0f;
1147 }
1148 else if (type == BTNodeType::Repeater)
1149 {
1150 newNode.repeatCount = 1;
1151 newNode.decoratorChildId = 0;
1152 }
1153 else if (type == BTNodeType::Inverter)
1154 {
1155 newNode.decoratorChildId = 0;
1156 }
1157
1158 nodes.push_back(newNode);
1159
1160 std::cout << "[BehaviorTreeAsset] Added node ID=" << newNode.id
1161 << " type=" << static_cast<int>(type) << " name='" << name << "'" << std::endl;
1162
1163 return newNode.id;
1164}
1165
1167{
1168 // Find and remove the node
1169 auto it = nodes.begin();
1170 while (it != nodes.end())
1171 {
1172 if (it->id == nodeId)
1173 {
1174 nodes.erase(it);
1175 break;
1176 }
1177 ++it;
1178 }
1179
1180 // Clean up connections to/from this node
1181 for (auto& node : nodes)
1182 {
1183 // Remove from childIds
1184 auto childIt = node.childIds.begin();
1185 while (childIt != node.childIds.end())
1186 {
1187 if (*childIt == nodeId)
1188 {
1189 childIt = node.childIds.erase(childIt);
1190 }
1191 else
1192 {
1193 ++childIt;
1194 }
1195 }
1196
1197 // Remove from decoratorChildId
1198 if (node.decoratorChildId == nodeId)
1199 {
1200 node.decoratorChildId = 0;
1201 }
1202 }
1203
1204 std::cout << "[BehaviorTreeAsset] Removed node ID=" << nodeId << std::endl;
1205 return true;
1206}
1207
1209{
1210 BTNode* parent = GetNode(parentId);
1211 const BTNode* child = GetNode(childId);
1212
1213 if (!parent || !child)
1214 {
1215 std::cerr << "[BehaviorTreeAsset] ConnectNodes failed: invalid node IDs" << std::endl;
1216 return false;
1217 }
1218
1219 // Add connection based on parent type
1220 if (parent->type == BTNodeType::Selector || parent->type == BTNodeType::Sequence)
1221 {
1222 // Check if already connected
1223 for (uint32_t id : parent->childIds)
1224 {
1225 if (id == childId)
1226 {
1227 std::cerr << "[BehaviorTreeAsset] Connection already exists" << std::endl;
1228 return false;
1229 }
1230 }
1231
1232 parent->childIds.push_back(childId);
1233 std::cout << "[BehaviorTreeAsset] Connected composite node " << parentId << " -> " << childId << std::endl;
1234 return true;
1235 }
1236 else if (parent->type == BTNodeType::Inverter || parent->type == BTNodeType::Repeater)
1237 {
1238 if (parent->decoratorChildId != 0)
1239 {
1240 std::cerr << "[BehaviorTreeAsset] Decorator already has a child" << std::endl;
1241 return false;
1242 }
1243
1244 parent->decoratorChildId = childId;
1245 std::cout << "[BehaviorTreeAsset] Connected decorator node " << parentId << " -> " << childId << std::endl;
1246 return true;
1247 }
1248 else
1249 {
1250 std::cerr << "[BehaviorTreeAsset] Cannot connect from leaf node" << std::endl;
1251 return false;
1252 }
1253}
1254
1256{
1257 BTNode* parent = GetNode(parentId);
1258
1259 if (!parent)
1260 {
1261 return false;
1262 }
1263
1264 // Remove from childIds
1265 auto it = parent->childIds.begin();
1266 while (it != parent->childIds.end())
1267 {
1268 if (*it == childId)
1269 {
1270 parent->childIds.erase(it);
1271 std::cout << "[BehaviorTreeAsset] Disconnected " << parentId << " -X-> " << childId << std::endl;
1272 return true;
1273 }
1274 ++it;
1275 }
1276
1277 // Remove from decoratorChildId
1278 if (parent->decoratorChildId == childId)
1279 {
1280 parent->decoratorChildId = 0;
1281 std::cout << "[BehaviorTreeAsset] Disconnected decorator " << parentId << " -X-> " << childId << std::endl;
1282 return true;
1283 }
1284
1285 return false;
1286}
std::cout<< "[BTEditor] Duplicated node: "<< duplicate.name<< " (ID: "<< duplicate.id<< ")"<< std::endl;} } m_selectedNodes=newNodes;m_treeModified=true;m_currentLayout=m_layoutEngine.ComputeLayout(&m_editingTree, m_nodeSpacingX, m_nodeSpacingY, m_currentZoom);m_validationMessages=m_editingTree.ValidateTreeFull();} bool BehaviorTreeDebugWindow::ValidateConnection(uint32_t parentId, uint32_t childId) const { const BTNode *parent=m_editingTree.GetNode(parentId);const BTNode *child=m_editingTree.GetNode(childId);if(!parent||!child) return false;if(parentId==childId) return false;if(parent->type !=BTNodeType::Selector &&parent->type !=BTNodeType::Sequence &&parent->type !=BTNodeType::Inverter &&parent->type !=BTNodeType::Repeater) { return false;} if((parent->type==BTNodeType::Inverter||parent->type==BTNodeType::Repeater) &&parent->decoratorChildId !=0) { return false;} if(parent->type==BTNodeType::Selector||parent->type==BTNodeType::Sequence) { if(std::find(parent->childIds.begin(), parent->childIds.end(), childId) !=parent->childIds.end()) { return false;} } std::vector< uint32_t > visited
BTStatus ExecuteBTAction(BTActionType actionType, float param1, float param2, EntityID entity, AIBlackboard_data &blackboard)
nlohmann::json json
BTStatus ExecuteBTNode(const BTNode &node, EntityID entity, AIBlackboard_data &blackboard, const BehaviorTreeAsset &tree)
BTStatus ExecuteBTCondition(BTConditionType condType, float param, EntityID entity, const AIBlackboard_data &blackboard)
Data-driven behavior tree system for AI decision making.
BTActionType
Built-in action types for behavior trees.
@ SetMoveGoalToTarget
Move towards current target.
@ SetMoveGoalToLastKnownTargetPos
Move to last seen target position.
@ WaitRandomTime
Initialize random timer (param1=min, param2=max)
@ MoveToGoal
Execute movement to goal.
@ ClearTarget
Clear current target.
@ PatrolPickNextPoint
Select next patrol point.
@ ChooseRandomNavigablePoint
Choose navigable point (param1=searchRadius, param2=maxAttempts)
@ AttackIfClose
Attack if in range.
@ Idle
Do nothing.
@ SetMoveGoalToPatrolPoint
Move to next patrol waypoint.
@ RequestPathfinding
Request pathfinding to moveGoal via MoveIntent.
@ FollowPath
Follow the path (check progression)
BTStatus
Behavior tree node execution status.
@ Success
Node completed successfully.
@ Running
Node is currently executing.
@ Failure
Node failed.
BTNodeType
Behavior tree node types.
@ Action
Leaf node - performs an action.
@ Selector
OR node - succeeds if any child succeeds.
@ Sequence
AND node - succeeds if all children succeed.
@ Inverter
Decorator - inverts child result.
@ Condition
Leaf node - checks a condition.
@ Repeater
Decorator - repeats child N times.
BTConditionType
Built-in condition types for behavior trees.
@ HasValidPath
Valid path calculated?
@ CanAttack
Attack is available.
@ IsWaitTimerExpired
Wait timer expired?
@ HeardNoise
Detected noise.
@ TargetVisible
Can see target entity.
@ HasMoveGoal
Movement goal is set.
@ HasNavigableDestination
Navigable destination chosen?
@ HealthBelow
Health below threshold.
@ HasReachedDestination
Reached destination?
@ TargetInRange
Target within specified range.
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
static uint32_t GenerateTreeIdFromPath(const std::string &treePath)
bool LoadTreeFromFile(const std::string &filepath, uint32_t treeId)
std::string GetTreePathFromId(uint32_t treeId) const
void DebugPrintLoadedTrees() const
uint32_t GetTreeIdFromPath(const std::string &treePath) const
const BehaviorTreeAsset * GetTree(uint32_t treeId) const
std::vector< BehaviorTreeAsset > m_trees
bool IsTreeLoadedByPath(const std::string &treePath) const
bool ValidateTree(const BehaviorTreeAsset &tree, std::string &errorMessage) const
std::map< std::string, uint32_t > m_pathToIdMap
bool ReloadTree(uint32_t treeId)
const BehaviorTreeAsset * GetTreeByPath(const std::string &treePath) const
const BehaviorTreeAsset * GetTreeByAnyId(uint32_t treeId) const
static float fDt
Delta time between frames in seconds.
Definition GameEngine.h:120
bool GetRandomNavigablePoint(float centerX, float centerY, float radius, int maxAttempts, float &outX, float &outY, CollisionLayer layer=CollisionLayer::Ground) const
static NavigationMap & Get()
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
T & GetComponent(EntityID entity)
Definition World.h:438
std::string GetString(const json &j, const std::string &key, const std::string &defaultValue="")
Safely get a string value from JSON.
uint32_t GetUInt(const json &j, const std::string &key, uint32_t defaultValue=0)
Safely get an unsigned integer value from JSON.
bool LoadJsonFromFile(const std::string &filepath, json &j)
Load and parse a JSON file.
Definition json_helper.h:42
int GetInt(const json &j, const std::string &key, int defaultValue=0)
Safely get an integer value from JSON.
void ForEachInArray(const json &j, const std::string &key, std::function< void(const json &, size_t)> callback)
Iterate over an array with a callback function.
float GetFloat(const json &j, const std::string &key, float defaultValue=0.0f)
Safely get a float value from JSON.
bool IsArray(const json &j, const std::string &key)
Check if a key contains an array.
nlohmann::json json
Represents a single node in a behavior tree.
std::vector< uint32_t > childIds
IDs of child nodes.
uint32_t id
Unique node ID within tree.
std::string name
uint32_t decoratorChildId
BTNodeType type
Node type.
Validation message for behavior tree structure checking.
uint32_t AddNode(BTNodeType type, const std::string &name, const Vector &position)
bool RemoveNode(uint32_t nodeId)
std::vector< BTValidationMessage > ValidateTreeFull() const
bool ConnectNodes(uint32_t parentId, uint32_t childId)
uint32_t GenerateNextNodeId() const
std::vector< BTNode > nodes
BTNode * GetNode(uint32_t nodeId)
bool DetectCycle(uint32_t startNodeId) const
bool DisconnectNodes(uint32_t parentId, uint32_t childId)
Position component for spatial location.
Vector position
2D/3D position vector
#define SYSTEM_LOG