Publicado: 2026-07-28 | Por: Carlos Montiel | Lectura: ~4 minutos
Pong es el "hola mundo" de los videojuegos. Si entiendes cómo construirlo desde cero, entiendes la estructura que comparten prácticamente todos los juegos en tiempo real.
Estructura del proyecto
En la lección anterior dejamos el entorno listo con SDL2 y CMake. Ahora vamos a usar exactamente esa base para construir Pong: dos paletas, una pelota que rebota, y un marcador simple.
pong/
├── CMakeLists.txt
├── src/
│ └── main.cpp
└── build/
Todo el juego va a vivir en un único `main.cpp` por simplicidad — en lecciones posteriores vamos a separar el código en múltiples archivos cuando el proyecto lo justifique. Por ahora, la prioridad es que entiendas el flujo completo sin saltar entre archivos.
El bucle principal del juego
Todo videojuego, sin importar su complejidad, tiene el mismo esqueleto: procesar input, actualizar el estado del mundo, y dibujar. Esto se llama el "game loop", y es probablemente el concepto más importante de todo este curso.
#include <SDL2/SDL.h>
#include <iostream>
const int ANCHO = 800;
const int ALTO = 600;
int main(int argc, char* argv[]) {
SDL_Init(SDL_INIT_VIDEO);
SDL_Window* ventana = SDL_CreateWindow("Pong", SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED, ANCHO, ALTO, SDL_WINDOW_SHOWN);
SDL_Renderer* renderer = SDL_CreateRenderer(ventana, -1, SDL_RENDERER_ACCELERATED);
bool corriendo = true;
Uint32 tiempo_anterior = SDL_GetTicks();
while (corriendo) {
// 1. Procesar input
SDL_Event evento;
while (SDL_PollEvent(&evento)) {
if (evento.type == SDL_QUIT) corriendo = false;
}
// 2. Calcular delta time (tiempo entre fotogramas)
Uint32 tiempo_actual = SDL_GetTicks();
float delta = (tiempo_actual - tiempo_anterior) / 1000.0f;
tiempo_anterior = tiempo_actual;
// 3. Actualizar (aquí irá la lógica del juego)
// 4. Dibujar
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
SDL_RenderClear(renderer);
SDL_RenderPresent(renderer);
SDL_Delay(16); // aproximadamente 60 fotogramas por segundo
}
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(ventana);
SDL_Quit();
return 0;
}
El `delta` (tiempo transcurrido entre fotograma y fotograma) es clave: en vez de mover objetos una cantidad fija de píxeles por fotograma, los movemos una cantidad proporcional al tiempo real transcurrido. Esto hace que el juego se comporte igual en una computadora rápida que en una lenta.
Definiendo las paletas y la pelota
Vamos a representar cada elemento del juego con un `SDL_Rect` (para las paletas, que son rectángulos) y variables de posición y velocidad para la pelota.
struct Paleta {
SDL_Rect rect;
float velocidad_y = 0.0f;
const float VELOCIDAD = 400.0f; // píxeles por segundo
};
struct Pelota {
SDL_Rect rect;
float vel_x = 300.0f;
float vel_y = 200.0f;
};
Paleta jugador1 { { 30, ALTO / 2 - 50, 15, 100 } };
Paleta jugador2 { { ANCHO - 45, ALTO / 2 - 50, 15, 100 } };
Pelota pelota { { ANCHO / 2 - 8, ALTO / 2 - 8, 16, 16 } };
Detectando el input del jugador
Para un control más fluido que reaccionar a eventos discretos, usamos `SDL_GetKeyboardState`, que nos da el estado de todas las teclas en cada fotograma.
const Uint8* teclado = SDL_GetKeyboardState(nullptr);
// Jugador 1: W / S
jugador1.velocidad_y = 0.0f;
if (teclado[SDL_SCANCODE_W]) jugador1.velocidad_y = -jugador1.VELOCIDAD;
if (teclado[SDL_SCANCODE_S]) jugador1.velocidad_y = jugador1.VELOCIDAD;
// Jugador 2: flechas arriba / abajo
jugador2.velocidad_y = 0.0f;
if (teclado[SDL_SCANCODE_UP]) jugador2.velocidad_y = -jugador2.VELOCIDAD;
if (teclado[SDL_SCANCODE_DOWN]) jugador2.velocidad_y = jugador2.VELOCIDAD;
jugador1.rect.y += static_cast<int>(jugador1.velocidad_y * delta);
jugador2.rect.y += static_cast<int>(jugador2.velocidad_y * delta);
// Evitar que las paletas salgan de la pantalla
if (jugador1.rect.y < 0) jugador1.rect.y = 0;
if (jugador1.rect.y + jugador1.rect.h > ALTO) jugador1.rect.y = ALTO - jugador1.rect.h;
if (jugador2.rect.y < 0) jugador2.rect.y = 0;
if (jugador2.rect.y + jugador2.rect.h > ALTO) jugador2.rect.y = ALTO - jugador2.rect.h;
Moviendo la pelota y rebotando en los bordes
pelota.rect.x += static_cast<int>(pelota.vel_x * delta);
pelota.rect.y += static_cast<int>(pelota.vel_y * delta);
// Rebote arriba/abajo
if (pelota.rect.y <= 0 || pelota.rect.y + pelota.rect.h >= ALTO) {
pelota.vel_y = -pelota.vel_y;
}
// Punto para un jugador (la pelota sale por izquierda o derecha)
if (pelota.rect.x < 0) {
std::cout << "Punto para el jugador 2" << std::endl;
pelota.rect.x = ANCHO / 2 - 8;
pelota.rect.y = ALTO / 2 - 8;
pelota.vel_x = 300.0f;
}
if (pelota.rect.x > ANCHO) {
std::cout << "Punto para el jugador 1" << std::endl;
pelota.rect.x = ANCHO / 2 - 8;
pelota.rect.y = ALTO / 2 - 8;
pelota.vel_x = -300.0f;
}
Nota que todavía no detectamos colisión entre la pelota y las paletas — eso lo vamos a formalizar con matemática de colisiones en la lección 5. Por ahora, para tener Pong jugable hoy mismo, podemos hacer una comprobación simple con `SDL_HasIntersection`:
if (SDL_HasIntersection(&pelota.rect, &jugador1.rect) ||
SDL_HasIntersection(&pelota.rect, &jugador2.rect)) {
pelota.vel_x = -pelota.vel_x;
}
Dibujando todo en pantalla
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
SDL_RenderClear(renderer);
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
SDL_RenderFillRect(renderer, &jugador1.rect);
SDL_RenderFillRect(renderer, &jugador2.rect);
SDL_RenderFillRect(renderer, &pelota.rect);
SDL_RenderPresent(renderer);
Compilando y probando
mkdir build && cd build
cmake ..
cmake --build .
./juego
Deberías ver dos paletas blancas y una pelota moviéndose, con W/S y las flechas controlando cada lado. Si algo no compila, este es un excelente momento para practicar el flujo de trabajo con tu asistente de IA: pégale el error completo del compilador (no solo la última línea) y pídele que te explique la causa antes de aplicar la corrección — así aprendes qué significaba el error, no solo cómo desapareció.
En la próxima lección vamos a reemplazar estos rectángulos blancos por sprites reales, cargando imágenes con SDL2_image y animándolas por fotogramas.