feat: Convertir BOIDS a sistema time-based (independiente de framerate)

- Conversión completa de físicas BOIDS de frame-based a time-based
- Velocidades: ×60 (px/frame → px/s)
- Aceleraciones (Separation, Cohesion): ×3600 (px/frame² → px/s²)
- Steering proporcional (Alignment): ×60
- Límites de velocidad: ×60

Constantes actualizadas en defines.h:
- BOID_SEPARATION_WEIGHT: 1.5 → 5400.0 (aceleración)
- BOID_COHESION_WEIGHT: 0.001 → 3.6 (aceleración)
- BOID_ALIGNMENT_WEIGHT: 1.0 → 60.0 (steering)
- BOID_MAX_SPEED: 2.5 → 150.0 px/s
- BOID_MIN_SPEED: 0.3 → 18.0 px/s
- BOID_MAX_FORCE: 0.05 → 3.0 px/s

Física ahora consistente en 60Hz, 144Hz, 240Hz screens.
Transiciones BOIDS↔PHYSICS preservan velocidad correctamente.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-17 20:05:49 +02:00
parent a929346463
commit 9909d4c12d
4 changed files with 68 additions and 20 deletions

View File

@@ -57,9 +57,9 @@ void BoidManager::activateBoids() {
float vx, vy; float vx, vy;
ball->getVelocity(vx, vy); ball->getVelocity(vx, vy);
if (vx == 0.0f && vy == 0.0f) { if (vx == 0.0f && vy == 0.0f) {
// Velocidad aleatoria entre -1 y 1 // Velocidad aleatoria entre -60 y +60 px/s (time-based)
vx = (rand() % 200 - 100) / 100.0f; vx = ((rand() % 200 - 100) / 100.0f) * 60.0f;
vy = (rand() % 200 - 100) / 100.0f; vy = ((rand() % 200 - 100) / 100.0f) * 60.0f;
ball->setVelocity(vx, vy); ball->setVelocity(vx, vy);
} }
} }
@@ -118,14 +118,14 @@ void BoidManager::update(float delta_time) {
limitSpeed(ball.get()); limitSpeed(ball.get());
} }
// Actualizar posiciones con velocidades resultantes // Actualizar posiciones con velocidades resultantes (time-based)
for (auto& ball : balls) { for (auto& ball : balls) {
float vx, vy; float vx, vy;
ball->getVelocity(vx, vy); ball->getVelocity(vx, vy);
SDL_FRect pos = ball->getPosition(); SDL_FRect pos = ball->getPosition();
pos.x += vx; pos.x += vx * delta_time; // time-based
pos.y += vy; pos.y += vy * delta_time;
ball->setPosition(pos.x, pos.y); ball->setPosition(pos.x, pos.y);
} }

View File

@@ -289,16 +289,21 @@ constexpr float LOGO_FLIP_TRIGGER_MAX = 0.80f; // 80% máximo de progres
constexpr int LOGO_FLIP_WAIT_PROBABILITY = 50; // 50% probabilidad de elegir el camino "esperar flip" constexpr int LOGO_FLIP_WAIT_PROBABILITY = 50; // 50% probabilidad de elegir el camino "esperar flip"
// Configuración de Modo BOIDS (comportamiento de enjambre) // Configuración de Modo BOIDS (comportamiento de enjambre)
// FASE 1.1 REVISADA: Parámetros ajustados tras detectar cohesión mal normalizada // TIME-BASED CONVERSION (frame-based → time-based):
// - Radios: sin cambios (píxeles)
// - Velocidades (MAX_SPEED, MIN_SPEED): ×60 (px/frame → px/s)
// - Aceleraciones puras (SEPARATION, COHESION): ×60² = ×3600 (px/frame² → px/s²)
// - Steering proporcional (ALIGNMENT): ×60 (proporcional a velocidad)
// - Límite velocidad (MAX_FORCE): ×60 (px/frame → px/s)
constexpr float BOID_SEPARATION_RADIUS = 30.0f; // Radio para evitar colisiones (píxeles) constexpr float BOID_SEPARATION_RADIUS = 30.0f; // Radio para evitar colisiones (píxeles)
constexpr float BOID_ALIGNMENT_RADIUS = 50.0f; // Radio para alinear velocidad con vecinos constexpr float BOID_ALIGNMENT_RADIUS = 50.0f; // Radio para alinear velocidad con vecinos
constexpr float BOID_COHESION_RADIUS = 80.0f; // Radio para moverse hacia centro del grupo constexpr float BOID_COHESION_RADIUS = 80.0f; // Radio para moverse hacia centro del grupo
constexpr float BOID_SEPARATION_WEIGHT = 1.5f; // Peso de separación constexpr float BOID_SEPARATION_WEIGHT = 5400.0f; // Aceleración de separación (px/s²) [era 1.5 × 3600]
constexpr float BOID_ALIGNMENT_WEIGHT = 1.0f; // Peso de alineación constexpr float BOID_ALIGNMENT_WEIGHT = 60.0f; // Steering de alineación (proporcional) [era 1.0 × 60]
constexpr float BOID_COHESION_WEIGHT = 0.001f; // Peso de cohesión (MICRO - 1000x menor por falta de normalización) constexpr float BOID_COHESION_WEIGHT = 3.6f; // Aceleración de cohesión (px/s²) [era 0.001 × 3600]
constexpr float BOID_MAX_SPEED = 2.5f; // Velocidad máxima (píxeles/frame - REDUCIDA) constexpr float BOID_MAX_SPEED = 150.0f; // Velocidad máxima (px/s) [era 2.5 × 60]
constexpr float BOID_MAX_FORCE = 0.05f; // Fuerza máxima de steering (REDUCIDA para evitar aceleración excesiva) constexpr float BOID_MAX_FORCE = 3.0f; // Fuerza máxima de steering (px/s) [era 0.05 × 60]
constexpr float BOID_MIN_SPEED = 0.3f; // Velocidad mínima (evita boids estáticos) constexpr float BOID_MIN_SPEED = 18.0f; // Velocidad mínima (px/s) [era 0.3 × 60]
// FASE 2: Spatial Hash Grid para optimización O(n²) → O(n) // 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) constexpr float BOID_GRID_CELL_SIZE = 100.0f; // Tamaño de celda del grid (píxeles)

View File

@@ -364,18 +364,19 @@ void Engine::handleGravityToggle() {
} }
void Engine::handleGravityDirectionChange(GravityDirection direction, const char* notification_text) { void Engine::handleGravityDirectionChange(GravityDirection direction, const char* notification_text) {
// Si estamos en modo boids, salir a modo física primero // Si estamos en modo boids, salir a modo física primero PRESERVANDO VELOCIDAD
if (current_mode_ == SimulationMode::BOIDS) { if (current_mode_ == SimulationMode::BOIDS) {
toggleBoidsMode(); // Esto cambia a PHYSICS y activa gravedad current_mode_ = SimulationMode::PHYSICS;
// Continuar para aplicar la dirección de gravedad boid_manager_->deactivateBoids(false); // NO activar gravedad aún (preservar momentum)
scene_manager_->forceBallsGravityOn(); // Activar gravedad SIN impulsos (preserva velocidad)
} }
// Si estamos en modo figura, salir a modo física CON gravedad // Si estamos en modo figura, salir a modo física CON gravedad
if (current_mode_ == SimulationMode::SHAPE) { else if (current_mode_ == SimulationMode::SHAPE) {
toggleShapeModeInternal(); // Desactivar figura (activa gravedad automáticamente) toggleShapeModeInternal(); // Desactivar figura (activa gravedad automáticamente)
} else { } else {
scene_manager_->enableBallsGravityIfDisabled(); // Reactivar gravedad si estaba OFF scene_manager_->enableBallsGravityIfDisabled(); // Reactivar gravedad si estaba OFF
} }
scene_manager_->changeGravityDirection(direction); scene_manager_->changeGravityDirection(direction);
showNotificationForAction(notification_text); showNotificationForAction(notification_text);
} }
@@ -437,9 +438,9 @@ void Engine::toggleDepthZoom() {
// Boids (comportamiento de enjambre) // Boids (comportamiento de enjambre)
void Engine::toggleBoidsMode() { void Engine::toggleBoidsMode() {
if (current_mode_ == SimulationMode::BOIDS) { if (current_mode_ == SimulationMode::BOIDS) {
// Salir del modo boids // Salir del modo boids (velocidades ya son time-based, no requiere conversión)
current_mode_ = SimulationMode::PHYSICS; current_mode_ = SimulationMode::PHYSICS;
boid_manager_->deactivateBoids(); boid_manager_->deactivateBoids(false); // NO activar gravedad (preservar momentum)
} else { } else {
// Entrar al modo boids (desde PHYSICS o SHAPE) // Entrar al modo boids (desde PHYSICS o SHAPE)
if (current_mode_ == SimulationMode::SHAPE) { if (current_mode_ == SimulationMode::SHAPE) {
@@ -1359,6 +1360,18 @@ void Engine::executeDemoAction(bool is_lite) {
int valid_scenarios[] = {1, 2, 3, 4, 5}; int valid_scenarios[] = {1, 2, 3, 4, 5};
int new_scenario = valid_scenarios[rand() % 5]; int new_scenario = valid_scenarios[rand() % 5];
scene_manager_->changeScenario(new_scenario, current_mode_); scene_manager_->changeScenario(new_scenario, current_mode_);
// Si estamos en modo SHAPE, regenerar la figura con nuevo número de pelotas
if (current_mode_ == SimulationMode::SHAPE) {
generateShape();
// Activar atracción física en las bolas nuevas (crítico tras changeScenario)
auto& balls = scene_manager_->getBallsMutable();
for (auto& ball : balls) {
ball->enableShapeAttraction(true);
}
}
return; return;
} }
@@ -1573,6 +1586,15 @@ void Engine::executeExitLogoMode() {
clampShapeScale(); clampShapeScale();
generateShape(); generateShape();
// Activar atracción física si estamos en modo SHAPE
// (crítico para que las bolas se muevan hacia la figura restaurada)
if (current_mode_ == SimulationMode::SHAPE) {
auto& balls = scene_manager_->getBallsMutable();
for (auto& ball : balls) {
ball->enableShapeAttraction(true);
}
}
// Desactivar modo LOGO en PNG_SHAPE (volver a flip intervals normales) // Desactivar modo LOGO en PNG_SHAPE (volver a flip intervals normales)
if (active_shape_) { if (active_shape_) {
PNGShape* png_shape = dynamic_cast<PNGShape*>(active_shape_.get()); PNGShape* png_shape = dynamic_cast<PNGShape*>(active_shape_.get());

View File

@@ -277,6 +277,27 @@ void UIManager::renderDebugHUD(const Engine* engine,
text_renderer_debug_->printAbsolute(margin, left_y, simmode_text.c_str(), {0, 255, 255, 255}); // Cian text_renderer_debug_->printAbsolute(margin, left_y, simmode_text.c_str(), {0, 255, 255, 255}); // Cian
left_y += line_height; left_y += line_height;
// Número de pelotas (escenario actual)
size_t ball_count = scene_manager->getBallCount();
std::string balls_text;
if (ball_count >= 1000) {
// Formatear con separador de miles (ejemplo: 5,000 o 50,000)
std::string count_str = std::to_string(ball_count);
std::string formatted;
int digits = count_str.length();
for (int i = 0; i < digits; i++) {
if (i > 0 && (digits - i) % 3 == 0) {
formatted += ',';
}
formatted += count_str[i];
}
balls_text = "Balls: " + formatted;
} else {
balls_text = "Balls: " + std::to_string(ball_count);
}
text_renderer_debug_->printAbsolute(margin, left_y, balls_text.c_str(), {128, 255, 128, 255}); // Verde claro
left_y += line_height;
// V-Sync // V-Sync
text_renderer_debug_->printAbsolute(margin, left_y, vsync_text_.c_str(), {0, 255, 255, 255}); // Cian text_renderer_debug_->printAbsolute(margin, left_y, vsync_text_.c_str(), {0, 255, 255, 255}); // Cian
left_y += line_height; left_y += line_height;