From abbda0f30b76b3778375e9b5f27bf6c64bf2962f Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 12 Oct 2025 05:46:34 +0200 Subject: [PATCH] =?UTF-8?q?FASE=202:=20Spatial=20Hash=20Grid=20-=20Optimiz?= =?UTF-8?q?aci=C3=B3n=20O(n=C2=B2)=20=E2=86=92=20O(n)=20para=20boids?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implementado sistema genérico de particionamiento espacial reutilizable que reduce drásticamente la complejidad del algoritmo de boids. **MEJORA DE RENDIMIENTO ESPERADA:** - Sin grid: 1000 boids = 1M comparaciones (1000²) - Con grid: 1000 boids ≈ 9K comparaciones (~9 vecinos/celda) - **Speedup teórico: ~100x en casos típicos** **COMPONENTES IMPLEMENTADOS:** 1. **SpatialGrid genérico (spatial_grid.h/.cpp):** - Divide espacio 2D en celdas de 100x100px - Hash map para O(1) lookup de celdas - queryRadius(): Busca solo en celdas adyacentes (máx 9 celdas) - Reutilizable para colisiones ball-to-ball en física (futuro) 2. **Integración en BoidManager:** - Grid poblado al inicio de cada frame (O(n)) - 3 reglas de Reynolds ahora usan queryRadius() en lugar de iterar TODOS - Separación/Alineación/Cohesión: O(n) total en lugar de O(n²) 3. **Configuración (defines.h):** - BOID_GRID_CELL_SIZE = 100.0f (≥ BOID_COHESION_RADIUS) **CAMBIOS TÉCNICOS:** - boid_manager.h: Añadido miembro spatial_grid_ - boid_manager.cpp: update() poblа grid, 3 reglas usan queryRadius() - spatial_grid.cpp: 89 líneas de implementación genérica - spatial_grid.h: 74 líneas con documentación exhaustiva **PRÓXIMOS PASOS:** - Medir rendimiento real con 1K, 5K, 10K boids - Comparar FPS antes/después - Validar que comportamiento es idéntico 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- source/boids_mgr/boid_manager.cpp | 43 +++++++++++---- source/boids_mgr/boid_manager.h | 7 ++- source/defines.h | 4 ++ source/spatial_grid.cpp | 89 +++++++++++++++++++++++++++++++ source/spatial_grid.h | 74 +++++++++++++++++++++++++ 5 files changed, 206 insertions(+), 11 deletions(-) create mode 100644 source/spatial_grid.cpp create mode 100644 source/spatial_grid.h diff --git a/source/boids_mgr/boid_manager.cpp b/source/boids_mgr/boid_manager.cpp index 01f172d..114a380 100644 --- a/source/boids_mgr/boid_manager.cpp +++ b/source/boids_mgr/boid_manager.cpp @@ -16,7 +16,8 @@ BoidManager::BoidManager() , state_mgr_(nullptr) , screen_width_(0) , screen_height_(0) - , boids_active_(false) { + , boids_active_(false) + , spatial_grid_(800, 600, BOID_GRID_CELL_SIZE) { // Tamaño por defecto, se actualiza en initialize() } BoidManager::~BoidManager() { @@ -30,11 +31,17 @@ void BoidManager::initialize(Engine* engine, SceneManager* scene_mgr, UIManager* state_mgr_ = state_mgr; screen_width_ = screen_width; screen_height_ = screen_height; + + // Actualizar dimensiones del spatial grid + spatial_grid_.updateWorldSize(screen_width, screen_height); } void BoidManager::updateScreenSize(int width, int height) { screen_width_ = width; screen_height_ = height; + + // Actualizar dimensiones del spatial grid (FASE 2) + spatial_grid_.updateWorldSize(width, height); } void BoidManager::activateBoids() { @@ -92,7 +99,17 @@ void BoidManager::update(float delta_time) { auto& balls = scene_mgr_->getBallsMutable(); + // FASE 2: Poblar spatial grid al inicio de cada frame (O(n)) + spatial_grid_.clear(); + for (auto& ball : balls) { + SDL_FRect pos = ball->getPosition(); + float center_x = pos.x + pos.w / 2.0f; + float center_y = pos.y + pos.h / 2.0f; + spatial_grid_.insert(ball.get(), center_x, center_y); + } + // Aplicar las tres reglas de Reynolds a cada boid + // FASE 2: Ahora usa spatial grid para búsquedas O(1) en lugar de O(n) for (auto& ball : balls) { applySeparation(ball.get(), delta_time); applyAlignment(ball.get(), delta_time); @@ -128,9 +145,11 @@ void BoidManager::applySeparation(Ball* boid, float delta_time) { float center_x = pos.x + pos.w / 2.0f; float center_y = pos.y + pos.h / 2.0f; - const auto& balls = scene_mgr_->getBalls(); - for (const auto& other : balls) { - if (other.get() == boid) continue; // Ignorar a sí mismo + // FASE 2: Usar spatial grid para buscar solo vecinos cercanos (O(1) en lugar de O(n)) + auto neighbors = spatial_grid_.queryRadius(center_x, center_y, BOID_SEPARATION_RADIUS); + + for (Ball* other : neighbors) { + if (other == boid) continue; // Ignorar a sí mismo SDL_FRect other_pos = other->getPosition(); float other_x = other_pos.x + other_pos.w / 2.0f; @@ -174,9 +193,11 @@ void BoidManager::applyAlignment(Ball* boid, float delta_time) { float center_x = pos.x + pos.w / 2.0f; float center_y = pos.y + pos.h / 2.0f; - const auto& balls = scene_mgr_->getBalls(); - for (const auto& other : balls) { - if (other.get() == boid) continue; + // FASE 2: Usar spatial grid para buscar solo vecinos cercanos (O(1) en lugar de O(n)) + auto neighbors = spatial_grid_.queryRadius(center_x, center_y, BOID_ALIGNMENT_RADIUS); + + for (Ball* other : neighbors) { + if (other == boid) continue; SDL_FRect other_pos = other->getPosition(); float other_x = other_pos.x + other_pos.w / 2.0f; @@ -229,9 +250,11 @@ void BoidManager::applyCohesion(Ball* boid, float delta_time) { float center_x = pos.x + pos.w / 2.0f; float center_y = pos.y + pos.h / 2.0f; - const auto& balls = scene_mgr_->getBalls(); - for (const auto& other : balls) { - if (other.get() == boid) continue; + // FASE 2: Usar spatial grid para buscar solo vecinos cercanos (O(1) en lugar de O(n)) + auto neighbors = spatial_grid_.queryRadius(center_x, center_y, BOID_COHESION_RADIUS); + + for (Ball* other : neighbors) { + if (other == boid) continue; SDL_FRect other_pos = other->getPosition(); float other_x = other_pos.x + other_pos.w / 2.0f; diff --git a/source/boids_mgr/boid_manager.h b/source/boids_mgr/boid_manager.h index 11b6efe..1fe6c30 100644 --- a/source/boids_mgr/boid_manager.h +++ b/source/boids_mgr/boid_manager.h @@ -2,7 +2,8 @@ #include // for size_t -#include "../defines.h" // for SimulationMode, AppMode +#include "../defines.h" // for SimulationMode, AppMode +#include "../spatial_grid.h" // for SpatialGrid // Forward declarations class Engine; @@ -98,6 +99,10 @@ class BoidManager { // Estado del modo boids bool boids_active_; + // Spatial Hash Grid para optimización O(n²) → O(n) + // FASE 2: Grid reutilizable para búsquedas de vecinos + SpatialGrid spatial_grid_; + // Métodos privados para las reglas de Reynolds void applySeparation(Ball* boid, float delta_time); void applyAlignment(Ball* boid, float delta_time); diff --git a/source/defines.h b/source/defines.h index 5c0e7e7..8d7a553 100644 --- a/source/defines.h +++ b/source/defines.h @@ -300,6 +300,10 @@ constexpr float BOID_MAX_SPEED = 2.5f; // Velocidad máxima (píxe constexpr float BOID_MAX_FORCE = 0.05f; // Fuerza máxima de steering (REDUCIDA para evitar aceleración excesiva) constexpr float BOID_MIN_SPEED = 0.3f; // Velocidad mínima (evita boids estáticos) +// FASE 2: Spatial Hash Grid para optimización O(n²) → O(n) +constexpr float BOID_GRID_CELL_SIZE = 100.0f; // Tamaño de celda del grid (píxeles) + // Debe ser ≥ BOID_COHESION_RADIUS para funcionar correctamente + constexpr float PI = 3.14159265358979323846f; // Constante PI // Función auxiliar para obtener la ruta del directorio del ejecutable diff --git a/source/spatial_grid.cpp b/source/spatial_grid.cpp new file mode 100644 index 0000000..bab51b8 --- /dev/null +++ b/source/spatial_grid.cpp @@ -0,0 +1,89 @@ +#include "spatial_grid.h" + +#include // for std::max, std::min +#include // for std::floor, std::ceil + +#include "ball.h" // for Ball + +SpatialGrid::SpatialGrid(int world_width, int world_height, float cell_size) + : world_width_(world_width) + , world_height_(world_height) + , cell_size_(cell_size) { + // Calcular número de celdas en cada dimensión + grid_cols_ = static_cast(std::ceil(world_width / cell_size)); + grid_rows_ = static_cast(std::ceil(world_height / cell_size)); +} + +void SpatialGrid::clear() { + // Limpiar todos los vectores de celdas (O(n) donde n = número de celdas ocupadas) + cells_.clear(); +} + +void SpatialGrid::insert(Ball* ball, float x, float y) { + // Obtener coordenadas de celda + int cell_x, cell_y; + getCellCoords(x, y, cell_x, cell_y); + + // Generar hash key y añadir a la celda + int key = getCellKey(cell_x, cell_y); + cells_[key].push_back(ball); +} + +std::vector SpatialGrid::queryRadius(float x, float y, float radius) { + std::vector results; + + // Calcular rango de celdas a revisar (AABB del círculo de búsqueda) + int min_cell_x, min_cell_y, max_cell_x, max_cell_y; + getCellCoords(x - radius, y - radius, min_cell_x, min_cell_y); + getCellCoords(x + radius, y + radius, max_cell_x, max_cell_y); + + // Iterar sobre todas las celdas dentro del AABB + for (int cy = min_cell_y; cy <= max_cell_y; cy++) { + for (int cx = min_cell_x; cx <= max_cell_x; cx++) { + // Verificar que la celda está dentro del grid + if (cx < 0 || cx >= grid_cols_ || cy < 0 || cy >= grid_rows_) { + continue; + } + + // Obtener key de la celda + int key = getCellKey(cx, cy); + + // Si la celda existe en el mapa, añadir todos sus objetos + auto it = cells_.find(key); + if (it != cells_.end()) { + // Añadir todos los objetos de esta celda al resultado + results.insert(results.end(), it->second.begin(), it->second.end()); + } + } + } + + return results; +} + +void SpatialGrid::updateWorldSize(int world_width, int world_height) { + world_width_ = world_width; + world_height_ = world_height; + + // Recalcular dimensiones del grid + grid_cols_ = static_cast(std::ceil(world_width / cell_size_)); + grid_rows_ = static_cast(std::ceil(world_height / cell_size_)); + + // Limpiar grid (las posiciones anteriores ya no son válidas) + clear(); +} + +// ============================================================================ +// MÉTODOS PRIVADOS +// ============================================================================ + +void SpatialGrid::getCellCoords(float x, float y, int& cell_x, int& cell_y) const { + // Convertir coordenadas del mundo a coordenadas de celda + cell_x = static_cast(std::floor(x / cell_size_)); + cell_y = static_cast(std::floor(y / cell_size_)); +} + +int SpatialGrid::getCellKey(int cell_x, int cell_y) const { + // Hash espacial 2D → 1D usando codificación por filas + // Formula: key = y * ancho + x (similar a array 2D aplanado) + return cell_y * grid_cols_ + cell_x; +} diff --git a/source/spatial_grid.h b/source/spatial_grid.h new file mode 100644 index 0000000..75a7caa --- /dev/null +++ b/source/spatial_grid.h @@ -0,0 +1,74 @@ +#ifndef SPATIAL_GRID_H +#define SPATIAL_GRID_H + +#include +#include + +class Ball; // Forward declaration + +// ============================================================================ +// SPATIAL HASH GRID - Sistema genérico de particionamiento espacial +// ============================================================================ +// +// Divide el espacio 2D en celdas de tamaño fijo para acelerar búsquedas de vecinos. +// Reduce complejidad de O(n²) a O(n) para queries de proximidad. +// +// CASOS DE USO: +// - Boids: Buscar vecinos para reglas de Reynolds (separación/alineación/cohesión) +// - Física: Detección de colisiones ball-to-ball (futuro) +// - IA: Pathfinding con obstáculos dinámicos +// +// ALGORITMO: +// 1. Dividir pantalla en grid de celdas (ej: 100x100px cada una) +// 2. Insertar cada Ball en celda(s) correspondiente(s) según posición +// 3. Query: Solo revisar celdas adyacentes (9 celdas max) en lugar de TODOS los objetos +// +// MEJORA DE RENDIMIENTO: +// - Sin grid: 1000 boids = 1M comparaciones (1000²) +// - Con grid: 1000 boids ≈ 9K comparaciones (1000 * ~9 vecinos/celda promedio) +// - Speedup: ~100x en casos típicos +// +// ============================================================================ + +class SpatialGrid { +public: + // Constructor: especificar dimensiones del mundo y tamaño de celda + SpatialGrid(int world_width, int world_height, float cell_size); + + // Limpiar todas las celdas (llamar al inicio de cada frame) + void clear(); + + // Insertar objeto en el grid según su posición (x, y) + void insert(Ball* ball, float x, float y); + + // Buscar todos los objetos dentro del radio especificado desde (x, y) + // Devuelve vector de punteros a Ball (puede contener duplicados si ball está en múltiples celdas) + std::vector queryRadius(float x, float y, float radius); + + // Actualizar dimensiones del mundo (útil para cambios de resolución F4) + void updateWorldSize(int world_width, int world_height); + +private: + // Convertir coordenadas (x, y) a índice de celda (cell_x, cell_y) + void getCellCoords(float x, float y, int& cell_x, int& cell_y) const; + + // Convertir (cell_x, cell_y) a hash key único para el mapa + int getCellKey(int cell_x, int cell_y) const; + + // Dimensiones del mundo (ancho/alto en píxeles) + int world_width_; + int world_height_; + + // Tamaño de cada celda (en píxeles) + float cell_size_; + + // Número de celdas en cada dimensión + int grid_cols_; + int grid_rows_; + + // Estructura de datos: hash map de cell_key → vector de Ball* + // Usamos unordered_map para O(1) lookup + std::unordered_map> cells_; +}; + +#endif // SPATIAL_GRID_H