#include "engine.h" #include // for SDL_GetError #include // for SDL_Event, SDL_PollEvent #include // for SDL_Init, SDL_Quit, SDL_INIT_VIDEO #include // for SDL_Keycode #include // for SDL_SetRenderDrawColor, SDL_RenderPresent #include // for SDL_GetTicks #include // for SDL_CreateWindow, SDL_DestroyWindow #include // for rand, srand #include // for time #include // for cout #include // for string #include "ball.h" // for Ball #include "external/dbgtxt.h" // for dbg_init, dbg_print #include "external/texture.h" // for Texture // Implementación de métodos públicos bool Engine::initialize() { bool success = true; if (!SDL_Init(SDL_INIT_VIDEO)) { std::cout << "¡SDL no se pudo inicializar! Error de SDL: " << SDL_GetError() << std::endl; success = false; } else { // Crear ventana principal window_ = SDL_CreateWindow(WINDOW_CAPTION, SCREEN_WIDTH * WINDOW_SIZE, SCREEN_HEIGHT * WINDOW_SIZE, SDL_WINDOW_OPENGL); if (window_ == nullptr) { std::cout << "¡No se pudo crear la ventana! Error de SDL: " << SDL_GetError() << std::endl; success = false; } else { // Crear renderizador renderer_ = SDL_CreateRenderer(window_, nullptr); if (renderer_ == nullptr) { std::cout << "¡No se pudo crear el renderizador! Error de SDL: " << SDL_GetError() << std::endl; success = false; } else { // Establecer color inicial del renderizador SDL_SetRenderDrawColor(renderer_, 0xFF, 0xFF, 0xFF, 0xFF); // Establecer tamaño lógico para el renderizado SDL_SetRenderLogicalPresentation(renderer_, SCREEN_WIDTH, SCREEN_HEIGHT, SDL_LOGICAL_PRESENTATION_INTEGER_SCALE); // Configurar V-Sync inicial SDL_SetRenderVSync(renderer_, vsync_enabled_ ? 1 : 0); } } } // Inicializar otros componentes si SDL se inicializó correctamente if (success) { texture_ = std::make_shared(renderer_, "data/ball.png"); srand(static_cast(time(nullptr))); dbg_init(renderer_); initBalls(scenario_); } return success; } void Engine::run() { while (!should_exit_) { calculateDeltaTime(); update(); handleEvents(); render(); } } void Engine::shutdown() { // Limpiar recursos SDL if (renderer_) { SDL_DestroyRenderer(renderer_); renderer_ = nullptr; } if (window_) { SDL_DestroyWindow(window_); window_ = nullptr; } SDL_Quit(); } // Métodos privados - esqueleto básico por ahora void Engine::calculateDeltaTime() { Uint64 current_time = SDL_GetTicks(); // En el primer frame, inicializar el tiempo anterior if (last_frame_time_ == 0) { last_frame_time_ = current_time; delta_time_ = 1.0f / 60.0f; // Asumir 60 FPS para el primer frame return; } // Calcular delta time en segundos delta_time_ = (current_time - last_frame_time_) / 1000.0f; last_frame_time_ = current_time; // Limitar delta time para evitar saltos grandes (pausa larga, depuración, etc.) if (delta_time_ > 0.05f) { // Máximo 50ms (20 FPS mínimo) delta_time_ = 1.0f / 60.0f; // Fallback a 60 FPS } } void Engine::update() { // Calcular FPS fps_frame_count_++; Uint64 current_time = SDL_GetTicks(); if (current_time - fps_last_time_ >= 1000) // Actualizar cada segundo { fps_current_ = fps_frame_count_; fps_frame_count_ = 0; fps_last_time_ = current_time; fps_text_ = "FPS: " + std::to_string(fps_current_); } // ¡DELTA TIME! Actualizar física siempre, usando tiempo transcurrido for (auto &ball : balls_) { ball->update(delta_time_); // Pasar delta time a cada pelota } // Actualizar texto (sin cambios en la lógica) if (show_text_) { show_text_ = !(SDL_GetTicks() - text_init_time_ > TEXT_DURATION); } } void Engine::handleEvents() { SDL_Event event; while (SDL_PollEvent(&event)) { // Salir del bucle si se detecta una petición de cierre if (event.type == SDL_EVENT_QUIT) { should_exit_ = true; break; } // Procesar eventos de teclado if (event.type == SDL_EVENT_KEY_DOWN && event.key.repeat == 0) { switch (event.key.key) { case SDLK_ESCAPE: should_exit_ = true; break; case SDLK_SPACE: pushUpBalls(); break; case SDLK_G: switchBallsGravity(); break; // Controles de dirección de gravedad con teclas de cursor case SDLK_UP: changeGravityDirection(GravityDirection::UP); break; case SDLK_DOWN: changeGravityDirection(GravityDirection::DOWN); break; case SDLK_LEFT: changeGravityDirection(GravityDirection::LEFT); break; case SDLK_RIGHT: changeGravityDirection(GravityDirection::RIGHT); break; case SDLK_V: toggleVSync(); break; case SDLK_H: show_debug_ = !show_debug_; break; case SDLK_T: // Ciclar al siguiente tema current_theme_ = static_cast((static_cast(current_theme_) + 1) % 4); initBalls(scenario_); // Regenerar bolas con nueva paleta break; case SDLK_F1: current_theme_ = ColorTheme::SUNSET; initBalls(scenario_); break; case SDLK_F2: current_theme_ = ColorTheme::OCEAN; initBalls(scenario_); break; case SDLK_F3: current_theme_ = ColorTheme::NEON; initBalls(scenario_); break; case SDLK_F4: current_theme_ = ColorTheme::FOREST; initBalls(scenario_); break; case SDLK_1: scenario_ = 0; initBalls(scenario_); break; case SDLK_2: scenario_ = 1; initBalls(scenario_); break; case SDLK_3: scenario_ = 2; initBalls(scenario_); break; case SDLK_4: scenario_ = 3; initBalls(scenario_); break; case SDLK_5: scenario_ = 4; initBalls(scenario_); break; case SDLK_6: scenario_ = 5; initBalls(scenario_); break; case SDLK_7: scenario_ = 6; initBalls(scenario_); break; case SDLK_8: scenario_ = 7; initBalls(scenario_); break; } } } } void Engine::render() { // Renderizar fondo degradado en lugar de color sólido renderGradientBackground(); // Limpiar batches del frame anterior batch_vertices_.clear(); batch_indices_.clear(); // Recopilar datos de todas las bolas para batch rendering for (auto &ball : balls_) { // En lugar de ball->render(), obtener datos para batch SDL_FRect pos = ball->getPosition(); Color color = ball->getColor(); addSpriteToBatch(pos.x, pos.y, pos.w, pos.h, color.r, color.g, color.b); } // Renderizar todas las bolas en una sola llamada if (!batch_vertices_.empty()) { SDL_RenderGeometry(renderer_, texture_->getSDLTexture(), batch_vertices_.data(), static_cast(batch_vertices_.size()), batch_indices_.data(), static_cast(batch_indices_.size())); } if (show_text_) { dbg_print(text_pos_, 8, text_.c_str(), 255, 255, 255); // Mostrar nombre del tema en castellano debajo del número de pelotas std::string theme_names_es[] = {"ATARDECER", "OCEANO", "NEON", "BOSQUE"}; std::string theme_name = theme_names_es[static_cast(current_theme_)]; int theme_text_width = static_cast(theme_name.length() * 8); // 8 píxeles por carácter int theme_x = (SCREEN_WIDTH - theme_text_width) / 2; // Centrar horizontalmente // Colores acordes a cada tema int theme_colors[][3] = { {255, 140, 60}, // ATARDECER: Naranja cálido {80, 200, 255}, // OCEANO: Azul océano {255, 60, 255}, // NEON: Magenta brillante {100, 255, 100} // BOSQUE: Verde natural }; int theme_idx = static_cast(current_theme_); dbg_print(theme_x, 24, theme_name.c_str(), theme_colors[theme_idx][0], theme_colors[theme_idx][1], theme_colors[theme_idx][2]); } // Debug display (solo si está activado con tecla H) if (show_debug_) { // Mostrar contador de FPS en esquina superior derecha int fps_text_width = static_cast(fps_text_.length() * 8); // 8 píxeles por carácter int fps_x = SCREEN_WIDTH - fps_text_width - 8; // 8 píxeles de margen dbg_print(fps_x, 8, fps_text_.c_str(), 255, 255, 0); // Amarillo para distinguir // Mostrar estado V-Sync en esquina superior izquierda dbg_print(8, 8, vsync_text_.c_str(), 0, 255, 255); // Cian para distinguir // Debug: Mostrar valores de la primera pelota (si existe) if (!balls_.empty()) { // Línea 1: Gravedad (solo números enteros) int grav_int = static_cast(balls_[0]->getGravityForce()); std::string grav_text = "GRAV " + std::to_string(grav_int); dbg_print(8, 24, grav_text.c_str(), 255, 0, 255); // Magenta para debug // Línea 2: Velocidad Y (solo números enteros) int vy_int = static_cast(balls_[0]->getVelocityY()); std::string vy_text = "VY " + std::to_string(vy_int); dbg_print(8, 32, vy_text.c_str(), 255, 0, 255); // Magenta para debug // Línea 3: Estado superficie std::string surface_text = balls_[0]->isOnSurface() ? "SURFACE YES" : "SURFACE NO"; dbg_print(8, 40, surface_text.c_str(), 255, 0, 255); // Magenta para debug // Línea 4: Coeficiente de rebote (loss) float loss_val = balls_[0]->getLossCoefficient(); std::string loss_text = "LOSS " + std::to_string(loss_val).substr(0, 4); // Solo 2 decimales dbg_print(8, 48, loss_text.c_str(), 255, 0, 255); // Magenta para debug // Línea 5: Dirección de gravedad std::string gravity_dir_text = "GRAVITY " + gravityDirectionToString(current_gravity_); dbg_print(8, 56, gravity_dir_text.c_str(), 255, 255, 0); // Amarillo para dirección } // Debug: Mostrar tema actual std::string theme_names[] = {"SUNSET", "OCEAN", "NEON", "FOREST"}; std::string theme_text = "THEME " + theme_names[static_cast(current_theme_)]; dbg_print(8, 64, theme_text.c_str(), 255, 255, 128); // Amarillo claro para tema } SDL_RenderPresent(renderer_); } void Engine::initBalls(int value) { // Limpiar las bolas actuales balls_.clear(); // Resetear gravedad al estado por defecto (DOWN) al cambiar escenario changeGravityDirection(GravityDirection::DOWN); // Crear las bolas según el escenario for (int i = 0; i < test_.at(value); ++i) { const int SIGN = ((rand() % 2) * 2) - 1; // Genera un signo aleatorio (+ o -) const float X = (rand() % (SCREEN_WIDTH / 2)) + (SCREEN_WIDTH / 4); // Posición inicial en X const float VX = (((rand() % 20) + 10) * 0.1f) * SIGN; // Velocidad en X const float VY = ((rand() % 60) - 30) * 0.1f; // Velocidad en Y // Seleccionar color de la paleta del tema actual ThemeColors &theme = themes_[static_cast(current_theme_)]; int color_index = rand() % 8; // 8 colores por tema const Color COLOR = {theme.ball_colors[color_index][0], theme.ball_colors[color_index][1], theme.ball_colors[color_index][2]}; // Generar factor de masa aleatorio (0.7 = ligera, 1.3 = pesada) float mass_factor = GRAVITY_MASS_MIN + (rand() % 1000) / 1000.0f * (GRAVITY_MASS_MAX - GRAVITY_MASS_MIN); balls_.emplace_back(std::make_unique(X, VX, VY, COLOR, texture_, current_gravity_, mass_factor)); } setText(); // Actualiza el texto } void Engine::setText() { int num_balls = test_.at(scenario_); if (num_balls == 1) { text_ = "1 PELOTA"; } else { text_ = std::to_string(num_balls) + " PELOTAS"; } text_pos_ = (SCREEN_WIDTH - static_cast(text_.length() * 8)) / 2; // Centrar texto show_text_ = true; text_init_time_ = SDL_GetTicks(); } void Engine::pushUpBalls() { for (auto &ball : balls_) { const int SIGNO = ((rand() % 2) * 2) - 1; const float VX = (((rand() % 20) + 10) * 0.1f) * SIGNO; const float VY = ((rand() % 40) * 0.1f) + 5; ball->modVel(VX, -VY); // Modifica la velocidad de la bola } } void Engine::switchBallsGravity() { for (auto &ball : balls_) { ball->switchGravity(); } } void Engine::changeGravityDirection(GravityDirection direction) { current_gravity_ = direction; for (auto &ball : balls_) { ball->setGravityDirection(direction); } } void Engine::toggleVSync() { vsync_enabled_ = !vsync_enabled_; vsync_text_ = vsync_enabled_ ? "VSYNC ON" : "VSYNC OFF"; // Aplicar el cambio de V-Sync al renderizador SDL_SetRenderVSync(renderer_, vsync_enabled_ ? 1 : 0); } std::string Engine::gravityDirectionToString(GravityDirection direction) const { switch (direction) { case GravityDirection::DOWN: return "DOWN"; case GravityDirection::UP: return "UP"; case GravityDirection::LEFT: return "LEFT"; case GravityDirection::RIGHT: return "RIGHT"; default: return "UNKNOWN"; } } void Engine::renderGradientBackground() { // Crear quad de pantalla completa con degradado SDL_Vertex bg_vertices[4]; // Obtener colores del tema actual ThemeColors &theme = themes_[static_cast(current_theme_)]; float top_r = theme.bg_top_r; float top_g = theme.bg_top_g; float top_b = theme.bg_top_b; float bottom_r = theme.bg_bottom_r; float bottom_g = theme.bg_bottom_g; float bottom_b = theme.bg_bottom_b; // Vértice superior izquierdo bg_vertices[0].position = {0, 0}; bg_vertices[0].tex_coord = {0.0f, 0.0f}; bg_vertices[0].color = {top_r, top_g, top_b, 1.0f}; // Vértice superior derecho bg_vertices[1].position = {SCREEN_WIDTH, 0}; bg_vertices[1].tex_coord = {1.0f, 0.0f}; bg_vertices[1].color = {top_r, top_g, top_b, 1.0f}; // Vértice inferior derecho bg_vertices[2].position = {SCREEN_WIDTH, SCREEN_HEIGHT}; bg_vertices[2].tex_coord = {1.0f, 1.0f}; bg_vertices[2].color = {bottom_r, bottom_g, bottom_b, 1.0f}; // Vértice inferior izquierdo bg_vertices[3].position = {0, SCREEN_HEIGHT}; bg_vertices[3].tex_coord = {0.0f, 1.0f}; bg_vertices[3].color = {bottom_r, bottom_g, bottom_b, 1.0f}; // Índices para 2 triángulos int bg_indices[6] = {0, 1, 2, 2, 3, 0}; // Renderizar sin textura (nullptr) SDL_RenderGeometry(renderer_, nullptr, bg_vertices, 4, bg_indices, 6); } void Engine::addSpriteToBatch(float x, float y, float w, float h, int r, int g, int b) { int vertex_index = static_cast(batch_vertices_.size()); // Crear 4 vértices para el quad (2 triángulos) SDL_Vertex vertices[4]; // Convertir colores de int (0-255) a float (0.0-1.0) float rf = r / 255.0f; float gf = g / 255.0f; float bf = b / 255.0f; // Vértice superior izquierdo vertices[0].position = {x, y}; vertices[0].tex_coord = {0.0f, 0.0f}; vertices[0].color = {rf, gf, bf, 1.0f}; // Vértice superior derecho vertices[1].position = {x + w, y}; vertices[1].tex_coord = {1.0f, 0.0f}; vertices[1].color = {rf, gf, bf, 1.0f}; // Vértice inferior derecho vertices[2].position = {x + w, y + h}; vertices[2].tex_coord = {1.0f, 1.0f}; vertices[2].color = {rf, gf, bf, 1.0f}; // Vértice inferior izquierdo vertices[3].position = {x, y + h}; vertices[3].tex_coord = {0.0f, 1.0f}; vertices[3].color = {rf, gf, bf, 1.0f}; // Añadir vértices al batch for (int i = 0; i < 4; i++) { batch_vertices_.push_back(vertices[i]); } // Añadir índices para 2 triángulos batch_indices_.push_back(vertex_index + 0); batch_indices_.push_back(vertex_index + 1); batch_indices_.push_back(vertex_index + 2); batch_indices_.push_back(vertex_index + 2); batch_indices_.push_back(vertex_index + 3); batch_indices_.push_back(vertex_index + 0); }