Olympe Engine 2.0
2D Game Engine with ECS Architecture
Loading...
Searching...
No Matches
CollisionMap.cpp
Go to the documentation of this file.
1/*
2Olympe Engine V2 - 2025
3CollisionMap & NavigationMap implementation
4Multi-layer collision and A* pathfinding system
5*/
6
7#include "CollisionMap.h"
8#include <limits>
9#include <unordered_set>
10
11// ============================================================================
12// COLLISION MAP IMPLEMENTATION
13// ============================================================================
14
15// Static member initialization
17
18void CollisionMap::Initialize(int width, int height, GridProjectionType projection,
19 float tileWidth, float tileHeight, int numLayers,
20 float tileOffsetX, float tileOffsetY)
21{
22 SYSTEM_LOG << "CollisionMap::Initialize(" << width << "x" << height << ", "
23 << numLayers << " layers, projection=" << static_cast<int>(projection) << ")\n";
24 SYSTEM_LOG << " -> Received tile dimensions: " << tileWidth << "x" << tileHeight << " px\n";
25
26 m_width = width;
27 m_height = height;
28 m_projection = projection;
29 m_tileWidth = tileWidth;
30 m_tileHeight = tileHeight;
35
36 SYSTEM_LOG << " -> Stored tile dimensions: m_tileWidth=" << m_tileWidth
37 << ", m_tileHeight=" << m_tileHeight << "\n";
38
39 // Log tile offset if present
40 if (m_tileOffsetX != 0.0f || m_tileOffsetY != 0.0f)
41 {
42 SYSTEM_LOG << " -> Tile offset: (" << m_tileOffsetX << ", " << m_tileOffsetY << ")\n";
43 }
44
45 // Allocate layers
46 m_layers.resize(numLayers);
47 for (int layer = 0; layer < numLayers; ++layer)
48 {
49 m_layers[layer].resize(height);
50 for (int y = 0; y < height; ++y)
51 {
52 m_layers[layer][y].resize(width);
53 // Initialize tiles with default properties
54 for (int x = 0; x < width; ++x)
55 {
56 m_layers[layer][y][x] = TileProperties();
57 m_layers[layer][y][x].layer = static_cast<CollisionLayer>(layer);
58 }
59 }
60 }
61
62 SYSTEM_LOG << " -> Allocated " << (width * height * numLayers) << " tiles\n";
63
64 // Pre-calculate world coordinates for all tiles (performance optimization)
65 for (int layer = 0; layer < numLayers; ++layer)
66 {
67 for (int y = 0; y < height; ++y)
68 {
69 for (int x = 0; x < width; ++x)
70 {
71 float worldX, worldY;
72 GridToWorld(x, y, worldX, worldY);
73
74 // Apply tile offset correction for isometric projection
76 {
77 worldX -= m_tileOffsetX;
78 worldY += m_tileOffsetY / 2.f;
79 }
80
81 m_layers[layer][y][x].worldX = worldX;
82 m_layers[layer][y][x].worldY = worldY;
83
84 #ifdef DEBUG_COLLISION_MAP_INIT
85 // Debug first few tiles in first layer to verify correct calculations
86 if (layer == 0 && y == 0 && x < 3)
87 {
88 SYSTEM_LOG << " [DEBUG] Tile (" << x << "," << y << ") -> world ("
89 << worldX << ", " << worldY << ")";
90 if (m_tileOffsetX != 0.0f || m_tileOffsetY != 0.0f)
91 {
92 SYSTEM_LOG << " [offset: (" << m_tileOffsetX << ", " << m_tileOffsetY << ")]";
93 }
94 SYSTEM_LOG << "\n";
95 }
96 #endif
97 }
98 }
99 }
100
101 SYSTEM_LOG << " -> Pre-calculated world coordinates for " << (width * height * numLayers) << " tiles\n";
102}
103
105{
106 if (static_cast<int>(layer) < m_numLayers)
107 {
108 m_activeLayer = layer;
109 }
110}
111
116
118{
119 return GetTileProperties(x, y, m_activeLayer);
120}
121
123{
124 if (IsValidGridPosition(x, y, layer))
125 {
126 int layerIdx = static_cast<int>(layer);
127 m_layers[layerIdx][y][x] = props;
128 m_layers[layerIdx][y][x].layer = layer;
129 }
130}
131
133{
134 if (IsValidGridPosition(x, y, layer))
135 {
136 int layerIdx = static_cast<int>(layer);
137 return m_layers[layerIdx][y][x];
138 }
139 return s_emptyTile;
140}
141
143{
144 if (IsValidGridPosition(x, y))
145 {
146 int layerIdx = static_cast<int>(m_activeLayer);
148 m_layers[layerIdx][y][x].isNavigable = !hasCollision;
149 }
150}
151
152bool CollisionMap::HasCollision(int x, int y) const
153{
154 return HasCollision(x, y, m_activeLayer);
155}
156
157bool CollisionMap::HasCollision(int x, int y, CollisionLayer layer) const
158{
159 if (IsValidGridPosition(x, y, layer))
160 {
161 int layerIdx = static_cast<int>(layer);
162 return m_layers[layerIdx][y][x].isBlocked;
163 }
164 return true; // Out of bounds = collision
165}
166
171
173{
174 if (IsValidGridPosition(x, y, layer) && updateFunc)
175 {
176 int layerIdx = static_cast<int>(layer);
178 }
179}
180
181void CollisionMap::WorldToGrid(float worldX, float worldY, int& outGridX, int& outGridY) const
182{
183 switch (m_projection)
184 {
186 // Orthogonal: direct mapping
187 outGridX = static_cast<int>(std::floor(worldX / m_tileWidth));
188 outGridY = static_cast<int>(std::floor(worldY / m_tileHeight));
189 break;
190
192 // Isometric: diamond transformation
193 // Convert world position to isometric grid coordinates
194 // Standard isometric: divide by half tile dimensions for proper scaling
195 {
196 float isoX = worldX / (m_tileWidth * 0.5f);
197 float isoY = worldY / (m_tileHeight * 0.5f);
198 outGridX = static_cast<int>(std::floor((isoX + isoY) * 0.5f));
199 outGridY = static_cast<int>(std::floor((isoY - isoX) * 0.5f));
200 }
201 break;
202
204 // Hexagonal (axial coordinates, pointy-top)
205 {
206 float q = (worldX * std::sqrt(3.0f) / 3.0f - worldY / 3.0f) / m_tileWidth;
207 float r = (worldY * 2.0f / 3.0f) / m_tileHeight;
208
209 // Cube coordinate conversion for rounding
210 float x = q;
211 float z = r;
212 float y = -x - z;
213
214 int rx = static_cast<int>(std::round(x));
215 int ry = static_cast<int>(std::round(y));
216 int rz = static_cast<int>(std::round(z));
217
218 float x_diff = std::abs(rx - x);
219 float y_diff = std::abs(ry - y);
220 float z_diff = std::abs(rz - z);
221
222 if (x_diff > y_diff && x_diff > z_diff)
223 rx = -ry - rz;
224 else if (y_diff > z_diff)
225 ry = -rx - rz;
226 else
227 rz = -rx - ry;
228
229 outGridX = rx;
230 outGridY = rz;
231 }
232 break;
233
234 default:
235 outGridX = 0;
236 outGridY = 0;
237 break;
238 }
239}
240
241void CollisionMap::GridToWorld(int gridX, int gridY, float& outWorldX, float& outWorldY) const
242{
243 switch (m_projection)
244 {
246 // Orthogonal: direct mapping (to tile center)
247 outWorldX = (gridX + 0.5f) * m_tileWidth;
248 outWorldY = (gridY + 0.5f) * m_tileHeight;
249 break;
250
252 // Isometric: diamond transformation (to tile center)
253 // Standard isometric formula with half tile dimensions
254 outWorldX = (gridX - gridY) * (m_tileWidth * 0.5f);
255 outWorldY = (gridX + gridY) * (m_tileHeight * 0.5f);
256 break;
257
259 // Hexagonal (axial coordinates, pointy-top) (to tile center)
260 {
261 float q = static_cast<float>(gridX);
262 float r = static_cast<float>(gridY);
263 outWorldX = m_tileWidth * (std::sqrt(3.0f) * q + std::sqrt(3.0f) / 2.0f * r);
264 outWorldY = m_tileHeight * (3.0f / 2.0f * r);
265 }
266 break;
267
268 default:
269 outWorldX = 0.0f;
270 outWorldY = 0.0f;
271 break;
272 }
273}
274
275bool CollisionMap::IsValidGridPosition(int x, int y) const
276{
277 return x >= 0 && x < m_width && y >= 0 && y < m_height;
278}
279
281{
282 int layerIdx = static_cast<int>(layer);
283 return IsValidGridPosition(x, y) && layerIdx >= 0 && layerIdx < m_numLayers;
284}
285
286const std::vector<std::vector<TileProperties>>& CollisionMap::GetLayer(CollisionLayer layer) const
287{
288 static const std::vector<std::vector<TileProperties>> emptyLayer;
289
290 int layerIdx = static_cast<int>(layer);
291 if (layerIdx >= 0 && layerIdx < static_cast<int>(m_layers.size()))
292 {
293 return m_layers[layerIdx];
294 }
295 return emptyLayer;
296}
297
298void CollisionMap::RegisterSector(int sectorX, int sectorY, int width, int height)
299{
301 sector.x = sectorX;
302 sector.y = sectorY;
303 sector.width = width;
304 sector.height = height;
305 sector.isLoaded = false;
306 sector.isActive = false;
307 m_sectors.push_back(sector);
308}
309
311{
312 for (Sector& sector : m_sectors)
313 {
314 if (sector.x == sectorX && sector.y == sectorY)
315 {
316 sector.isLoaded = true;
317 sector.isActive = true;
318 break;
319 }
320 }
321}
322
324{
325 for (Sector& sector : m_sectors)
326 {
327 if (sector.x == sectorX && sector.y == sectorY)
328 {
329 sector.isLoaded = false;
330 sector.isActive = false;
331 break;
332 }
333 }
334}
335
337{
338 m_layers.clear();
339 m_sectors.clear();
340 m_width = 0;
341 m_height = 0;
342 m_numLayers = 1;
344}
345
346// ============================================================================
347// NAVIGATION MAP IMPLEMENTATION
348// ============================================================================
349
350void NavigationMap::Initialize(int width, int height, GridProjectionType projection,
351 float tileWidth, float tileHeight, int numLayers)
352{
353 SYSTEM_LOG << "NavigationMap::Initialize(" << width << "x" << height << ", "
354 << numLayers << " layers)\n";
355
356 m_width = width;
357 m_height = height;
358 m_projection = projection;
359 m_tileWidth = tileWidth;
360 m_tileHeight = tileHeight;
363
364 // NavigationMap delegates to CollisionMap for tile storage
365 // We just need to ensure CollisionMap is initialized
366 SYSTEM_LOG << " -> NavigationMap ready (delegates to CollisionMap)\n";
367}
368
370{
371 if (static_cast<int>(layer) < m_numLayers)
372 {
373 m_activeLayer = layer;
374 }
375}
376
377void NavigationMap::SetNavigable(int x, int y, bool isNavigable, float cost)
378{
380 if (collMap.IsValidGridPosition(x, y, m_activeLayer))
381 {
382 TileProperties props = collMap.GetTileProperties(x, y, m_activeLayer);
383 props.isNavigable = isNavigable;
384 props.traversalCost = cost;
385 collMap.SetTileProperties(x, y, m_activeLayer, props);
386 }
387}
388
389bool NavigationMap::IsNavigable(int x, int y) const
390{
391 return IsNavigable(x, y, m_activeLayer);
392}
393
394float NavigationMap::GetTraversalCost(int x, int y) const
395{
396 return GetTraversalCost(x, y, m_activeLayer);
397}
398
399bool NavigationMap::IsNavigable(int x, int y, CollisionLayer layer) const
400{
402 if (collMap.IsValidGridPosition(x, y, layer))
403 {
404 const TileProperties& props = collMap.GetTileProperties(x, y, layer);
405 return props.isNavigable && !props.isBlocked;
406 }
407 return false;
408}
409
410float NavigationMap::GetTraversalCost(int x, int y, CollisionLayer layer) const
411{
413 if (collMap.IsValidGridPosition(x, y, layer))
414 {
415 const TileProperties& props = collMap.GetTileProperties(x, y, layer);
416 return props.traversalCost;
417 }
418 return std::numeric_limits<float>::max();
419}
420
421void NavigationMap::WorldToGrid(float worldX, float worldY, int& outGridX, int& outGridY) const
422{
424}
425
430
432{
433 return x >= 0 && x < m_width && y >= 0 && y < m_height;
434}
435
436float NavigationMap::Heuristic(int x1, int y1, int x2, int y2) const
437{
438 switch (m_projection)
439 {
441 // Manhattan distance (4-connected grid)
442 return static_cast<float>(std::abs(x2 - x1) + std::abs(y2 - y1));
443
445 // Diamond/Chebyshev distance (isometric grid)
446 return static_cast<float>(std::max(std::abs(x2 - x1), std::abs(y2 - y1)));
447
449 // Axial distance (hexagonal grid)
450 {
451 int dx = x2 - x1;
452 int dy = y2 - y1;
453 return static_cast<float>((std::abs(dx) + std::abs(dx + dy) + std::abs(dy)) / 2);
454 }
455
456 default:
457 return 0.0f;
458 }
459}
460
461void NavigationMap::GetNeighbors(int x, int y, std::vector<std::pair<int, int>>& outNeighbors) const
462{
463 outNeighbors.clear();
464
465 switch (m_projection)
466 {
468 // 4-connected (up, down, left, right)
469 outNeighbors.push_back(std::make_pair(x, y - 1)); // Up
470 outNeighbors.push_back(std::make_pair(x, y + 1)); // Down
471 outNeighbors.push_back(std::make_pair(x - 1, y)); // Left
472 outNeighbors.push_back(std::make_pair(x + 1, y)); // Right
473 break;
474
476 // 4-connected diamond (isometric)
477 outNeighbors.push_back(std::make_pair(x - 1, y)); // NW
478 outNeighbors.push_back(std::make_pair(x + 1, y)); // SE
479 outNeighbors.push_back(std::make_pair(x, y - 1)); // NE
480 outNeighbors.push_back(std::make_pair(x, y + 1)); // SW
481 break;
482
484 // 6-connected (hexagonal, pointy-top)
485 outNeighbors.push_back(std::make_pair(x + 1, y)); // E
486 outNeighbors.push_back(std::make_pair(x + 1, y - 1)); // NE
487 outNeighbors.push_back(std::make_pair(x, y - 1)); // NW
488 outNeighbors.push_back(std::make_pair(x - 1, y)); // W
489 outNeighbors.push_back(std::make_pair(x - 1, y + 1)); // SW
490 outNeighbors.push_back(std::make_pair(x, y + 1)); // SE
491 break;
492
493 default:
494 break;
495 }
496}
497
499 std::vector<Vector>& outPath, CollisionLayer layer, int maxIterations)
500{
501 outPath.clear();
502
503 // Validate positions
505 {
506 return false;
507 }
508
509 if (!IsNavigable(startX, startY, layer) || !IsNavigable(goalX, goalY, layer))
510 {
511 return false;
512 }
513
514 // Early exit if start == goal
515 if (startX == goalX && startY == goalY)
516 {
517 float worldX, worldY;
518 GridToWorld(startX, startY, worldX, worldY);
519 outPath.push_back(Vector(worldX, worldY, 0.0f));
520 return true;
521 }
522
523 // A* pathfinding with priority queue (min-heap)
524 struct NodeCompare
525 {
526 bool operator()(const PathNode* a, const PathNode* b) const
527 {
528 return a->fCost() > b->fCost(); // Min-heap (lower fCost = higher priority)
529 }
530 };
531
532 std::priority_queue<PathNode*, std::vector<PathNode*>, NodeCompare> openSet;
533 std::unordered_set<int> closedSet;
534 std::unordered_map<int, PathNode*> allNodes;
535
536 // Helper: encode position to unique int
537 auto encodePos = [this](int x, int y) -> int {
538 return y * m_width + x;
539 };
540
541 // Create start node
543 startNode->gCost = 0.0f;
545 startNode->parent = nullptr;
546
547 openSet.push(startNode);
549
550 PathNode* goalNode = nullptr;
551 int iterations = 0;
552
553 while (!openSet.empty() && iterations < maxIterations)
554 {
555 ++iterations;
556
557 // Get node with lowest fCost
558 PathNode* current = openSet.top();
559 openSet.pop();
560
561 int currentKey = encodePos(current->x, current->y);
562
563 // Skip if already processed
564 if (closedSet.find(currentKey) != closedSet.end())
565 {
566 continue;
567 }
568
569 closedSet.insert(currentKey);
570
571 // Check if goal reached
572 if (current->x == goalX && current->y == goalY)
573 {
575 break;
576 }
577
578 // Explore neighbors
579 std::vector<std::pair<int, int>> neighbors;
581
582 for (const std::pair<int, int>& neighbor : neighbors)
583 {
584 int nx = neighbor.first;
585 int ny = neighbor.second;
586 int neighborKey = encodePos(nx, ny);
587
588 // Skip if invalid or already processed
589 if (!IsValidGridPosition(nx, ny) || !IsNavigable(nx, ny, layer))
590 {
591 continue;
592 }
593
594 if (closedSet.find(neighborKey) != closedSet.end())
595 {
596 continue;
597 }
598
599 // Calculate tentative gCost
600 float moveCost = GetTraversalCost(nx, ny, layer);
601 float tentativeG = current->gCost + moveCost;
602
603 // Check if we found a better path to this neighbor
604 PathNode* neighborNode = nullptr;
605 if (allNodes.find(neighborKey) != allNodes.end())
606 {
608 if (tentativeG >= neighborNode->gCost)
609 {
610 continue; // Not a better path
611 }
612 }
613 else
614 {
615 neighborNode = new PathNode(nx, ny);
617 }
618
619 // Update node
620 neighborNode->gCost = tentativeG;
621 neighborNode->hCost = Heuristic(nx, ny, goalX, goalY);
622 neighborNode->parent = current;
623
624 openSet.push(neighborNode);
625 }
626 }
627
628 // Reconstruct path
629 bool pathFound = false;
630 if (goalNode != nullptr)
631 {
632 std::vector<PathNode*> pathNodes;
634 while (current != nullptr)
635 {
636 pathNodes.push_back(current);
638 }
639
640 // Reverse to get start->goal order
641 std::reverse(pathNodes.begin(), pathNodes.end());
642
643 // Convert to world coordinates
644 for (PathNode* node : pathNodes)
645 {
646 float worldX, worldY;
647 GridToWorld(node->x, node->y, worldX, worldY);
648 outPath.push_back(Vector(worldX, worldY, 0.0f));
649 }
650
651 pathFound = true;
652 }
653
654 // Cleanup
655 for (std::pair<const int, PathNode*>& entry : allNodes)
656 {
657 delete entry.second;
658 }
659
660 return pathFound;
661}
662
664{
665 m_width = 0;
666 m_height = 0;
667 m_numLayers = 1;
669}
670
672 int maxAttempts, float& outX, float& outY,
673 CollisionLayer layer) const
674{
675 for (int attempt = 0; attempt < maxAttempts; ++attempt)
676 {
677 // Generate random angle (0 to 2π)
678 float angle = (static_cast<float>(rand()) / static_cast<float>(RAND_MAX)) * 2.0f * 3.14159265f;
679
680 // Generate random distance (0 to radius)
681 // Use sqrt for uniform distribution
682 float randomRadius = std::sqrt(static_cast<float>(rand()) / static_cast<float>(RAND_MAX)) * radius;
683
684 // Calculate world position
685 float worldX = centerX + randomRadius * std::cos(angle);
686 float worldY = centerY + randomRadius * std::sin(angle);
687
688 // Convert to grid coordinates
689 int gridX, gridY;
690 WorldToGrid(worldX, worldY, gridX, gridY);
691
692 // Check if navigable
694 {
695 outX = worldX;
696 outY = worldY;
697 return true;
698 }
699 }
700
701 // Failed after maxAttempts attempts
702 return false;
703}
GridProjectionType
CollisionLayer
ComponentTypeID GetComponentTypeID_Static()
Definition ECS_Entity.h:56
void LoadSector(int sectorX, int sectorY)
std::vector< std::vector< std::vector< TileProperties > > > m_layers
static const TileProperties s_emptyTile
void GridToWorld(int gridX, int gridY, float &outWorldX, float &outWorldY) const
void SetActiveLayer(CollisionLayer layer)
const TileProperties & GetTileProperties(int x, int y) const
void RegisterSector(int sectorX, int sectorY, int width, int height)
CollisionLayer m_activeLayer
void SetCollision(int x, int y, bool hasCollision)
void UpdateTileState(int x, int y, TileUpdateFunc updateFunc)
const std::vector< std::vector< TileProperties > > & GetLayer(CollisionLayer layer) const
bool IsValidGridPosition(int x, int y) const
std::vector< Sector > m_sectors
void SetTileProperties(int x, int y, const TileProperties &props)
std::function< void(TileProperties &)> TileUpdateFunc
bool HasCollision(int x, int y) const
void Initialize(int width, int height, GridProjectionType projection, float tileWidth, float tileHeight, int numLayers=1, float tileOffsetX=0.0f, float tileOffsetY=0.0f)
GridProjectionType m_projection
float m_tileOffsetY
void UnloadSector(int sectorX, int sectorY)
static CollisionMap & Get()
float m_tileOffsetX
void WorldToGrid(float worldX, float worldY, int &outGridX, int &outGridY) const
bool FindPath(int startX, int startY, int goalX, int goalY, std::vector< Vector > &outPath, CollisionLayer layer=CollisionLayer::Ground, int maxIterations=10000)
void WorldToGrid(float worldX, float worldY, int &outGridX, int &outGridY) const
void SetNavigable(int x, int y, bool isNavigable, float cost=1.0f)
bool GetRandomNavigablePoint(float centerX, float centerY, float radius, int maxAttempts, float &outX, float &outY, CollisionLayer layer=CollisionLayer::Ground) const
float Heuristic(int x1, int y1, int x2, int y2) const
float GetTraversalCost(int x, int y) const
bool IsValidGridPosition(int x, int y) const
CollisionLayer m_activeLayer
void Initialize(int width, int height, GridProjectionType projection, float tileWidth, float tileHeight, int numLayers=1)
bool IsNavigable(int x, int y) const
void SetActiveLayer(CollisionLayer layer)
void GetNeighbors(int x, int y, std::vector< std::pair< int, int > > &outNeighbors) const
void GridToWorld(int gridX, int gridY, float &outWorldX, float &outWorldY) const
GridProjectionType m_projection
#define SYSTEM_LOG