Curso SFML C++: Animación 2D — una pelota que rebota en la ventana (2 versiones del código)

¿Qué vamos a hacer en esta lección?

En esta lección vamos a hacer un programa que simule un balón o pelota que rebota dentro de una ventana, como se muestra en la siguiente animación:
Pelota rebotando en una ventana SFML

Figura 1

Pantalla de gráficos

Los gráficos en el mundo virtual se basan en píxeles. Un píxel es un pequeño punto en la pantalla que tiene una posición, y sus valores se establecen con dos variables (coordenadas), como en matemáticas: x, y. El sistema de coordenadas en la pantalla de una computadora u otro dispositivo está establecido como lo muestra la figura 2. La coordenada 0,0 está en la esquina superior izquierda y la coordenada máxima está en la esquina inferior derecha. Por ejemplo, en la figura 2 vemos representada una pantalla o ventana con una resolución de 800x600 píxeles, sobre la cual se ve dibujado un rectángulo de lados iguales en la coordenada 400,300 y un píxel en la coordenada 75,75.
Sistema de coordenadas de la pantalla

Figura 2

La ilusión del movimiento

Para dibujar la figura en otro lugar de la pantalla o cambiar su posición, simplemente se modifican sus coordenadas (x, y). Para crear la ilusión del movimiento en la pantalla, se borra la figura anterior y luego se dibuja una en la nueva posición, bastante cerca de la anterior. La estrategia en SFML es dibujar-borrar-dibujar, y se repite indefinidamente hasta conseguir cierta condición controlada por la sentencia while (mientras que). Esta sentencia ejecuta una sección de código mientras la condición dada sea verdadera; de lo contrario, sale del bucle y continúa con el código siguiente, es decir, después de la llave de cierre }. Una condición while tiene la siguiente estructura:
while (condición)
{
    // sentencias
}
En los programas SFML de las lecciones anteriores ya hay una sentencia de control principal while en cuyo cuerpo se escriben todas las sentencias para dibujar. Dichas sentencias se ejecutan hasta que la condición dentro de los paréntesis sea falsa; es decir, hasta que la ventana se cierra. Ese bucle es el que vamos a utilizar para colocar todas nuestras sentencias de animación:
while (ventana.isOpen())
{
    1. Escuchamos los eventos para captar cuando el usuario cierre la ventana

    2. Limpiamos la pantalla

    3. Cambiamos los valores de las coordenadas x, y

    4. Pasamos las nuevas coordenadas x, y al objeto círculo

    5. Dibujamos en la ventana el círculo en la coordenada recibida

    6. Mostramos la ventana en la pantalla
}
Básicamente, las sentencias anteriores son las que necesitamos para crear una animación de una figura simple en 2D. Pero antes de escribir el código, vamos a analizar los detalles del movimiento.

Movimiento en línea recta y rebote en los bordes

Primero, vamos a crear una pantalla con un tamaño de 800 x 600 píxeles (ver figura 3). Por ejemplo, si queremos desplazar la figura desde la coordenada 0,0 hasta la coordenada 300,300, debemos incrementar equitativa y progresivamente los valores de x e y. Ahora, si seguimos incrementando los valores de x e y hasta que x >= 800 o y >= 600, la pelota simplemente desaparece de la pantalla. Incrementando de uno en uno las coordenadas x e y, es decir x = x + 1, y = y + 1, tenemos:
x = 0, 1, 2, 3, 4, ..., 600, 601, 602, ... 799, 800...
y = 0, 1, 2, 3, 4, ..., 600, 601, 602, ... 799, 800...
// La figura desaparece de la ventana cuando y >= 600
Como vemos, el incremento tanto para x como para y es de 1. Si inicialmente la figura está en la coordenada (0,0) y el incremento de x e y al mismo tiempo es de +1 píxel, el desplazamiento de la figura es en línea recta diagonal hacia abajo. Cuando y es igual o mayor a 600, la figura empieza a estar fuera del rango de la ventana y comienza a desaparecer. Para evitar que la figura se dibuje fuera de los límites de la ventana, establecemos límites a su trayectoria y la encerramos entre las 4 paredes o bordes de la ventana. Para ello usamos sentencias de control con las condiciones necesarias para cada límite:
izquierdo <= 0
derecho   >= 800
arriba    <= 0
abajo     >= 600
Al cumplirse alguna de estas condiciones, simplemente invertimos la dirección del objeto en movimiento cambiando el signo del incremento de la variable correspondiente (x o y), es decir, de +1 a -1 o de -1 a +1. En C++ (y en otros lenguajes) existe la sentencia de control if, que establece una condición que, de ser cierta, ejecuta las instrucciones dentro de su cuerpo:
if (x <= pared_izq)
    incremento_x = abs(incremento_x);

if (x >= pared_der)
    incremento_x = -incremento_x;

if (y <= techo)
    incremento_y = abs(incremento_y);

if (y >= piso)
    incremento_y = -incremento_y;
El bloque de condiciones anterior garantiza que la figura (pelota) se mantenga rebotando dentro de los límites establecidos por los valores de las paredes, el techo y el piso. Hay dos detalles adicionales: primero, para convertir un número positivo en negativo colocamos el signo menos (-) al lado del número o variable (-incremento_x); en segundo lugar, para convertir un número negativo en positivo usamos la función de valor absoluto abs(valor). La trayectoria de la figura en una ventana de lados diferentes debe verse como la figura 3:
Trayectoria de rebote en ventana 800x600

Figura 3

Si la ventana es de lados iguales, la trayectoria de la figura será la misma de ida y vuelta, como muestra la figura 4, ya que los límites de x e y son iguales si parte de 0,0 con incrementos iguales para ambas variables. Para que la trayectoria cambie, sólo basta con darle valores diferentes a los incrementos de las variables x, y. También, a medida que aumenta el valor del incremento, aumenta la velocidad del movimiento de la figura.
Trayectoria de rebote en ventana cuadrada

Figura 4

Velocidad de los fotogramas

Hay un detalle técnico importante que hay que tomar en cuenta: el control de la velocidad de los fotogramas. Debido a que el ciclo de borrado y dibujo es tan rápido, probablemente no veríamos nada en la pantalla. Para ello, sincronizamos con la frecuencia vertical del monitor, que es aproximadamente 60 fotogramas por segundo. ¿Cómo hacemos eso? Simplemente usamos las sentencias que proporciona SFML para tal fin y las escribimos justo después de crear la ventana:
ventana.setVerticalSyncEnabled(true);
Otras veces necesitamos que se ejecute a velocidades diferentes, y podemos usar la siguiente sentencia:
ventana.setFramerateLimit(30);

El código fuente (Versión 1.0)

#include <SFML/Graphics.hpp>

int main()
{
    // creamos la ventana de 800x600 pixeles
    sf::RenderWindow ventana(sf::VideoMode(800, 600), "Ventana de Dibujo");

    // definimos las variables
    int x = 1, y = 1;                 // coordenadas
    int incremento_x = 5, incremento_y = 5;
    int limite_izquierdo = 0, limite_derecho = 800;
    int limite_superior = 0, limite_inferior = 600;

    // creamos un objeto circulo con un radio 10
    // y definimos su color verde
    sf::CircleShape circulo1(10);
    circulo1.setFillColor(sf::Color::Green);

    // ejecutar el programa mientras la ventana esté abierta
    while (ventana.isOpen())
    {
        // verificamos todos los eventos de la ventana
        sf::Event evento;
        while (ventana.pollEvent(evento))
        {
            // evento "cierre solicitado": cerramos la ventana
            if (evento.type == sf::Event::Closed)
                ventana.close();
        }

        // limpiamos la ventana con el color negro
        ventana.clear(sf::Color::Black);

        // verificamos los bordes de la ventana y cambiamos
        // el signo de los incrementos
        if (x <= limite_izquierdo)
            incremento_x = abs(incremento_x);

        if (x >= limite_derecho)
            incremento_x = -incremento_x;

        if (y <= limite_superior)
            incremento_y = abs(incremento_y);

        if (y >= limite_inferior)
            incremento_y = -incremento_y;

        x = x + incremento_x;
        y = y + incremento_y;

        // pasamos las coordenadas actuales (x,y) al objeto circulo1
        circulo1.setPosition(x, y);

        // dibujamos el circulo en la ventana
        ventana.draw(circulo1);

        // mostramos la ventana en la pantalla
        ventana.display();
    }

    return 0;
}

El código fuente (Versión 2.0)

A continuación, una versión mejorada del código anterior, donde se utilizan las funciones incorporadas en SFML como mover (.move()) y obtener posición (.getPosition()):
// Programa en SFML y C++ que mueve una forma simple en pantalla (versión 2.0)

#include <SFML/Graphics.hpp>

int main()
{
    const int max_x = 800, max_y = 600, min_x = 0, min_y = 0;
    float r_ball = 10.f;              // radio del círculo
    float movx = 5.f, movy = 5.f;     // incremento de las coordenadas del círculo
    float x = 0, y = 0;               // coordenadas iniciales de ball

    sf::RenderWindow window(sf::VideoMode(max_x, max_y), "@intellvirtual: Mover Formas Simples");
    window.setVerticalSyncEnabled(true); // sincronismo vertical para controlar la velocidad de fotogramas

    sf::CircleShape ball(r_ball);
    ball.setFillColor(sf::Color::Yellow);
    ball.setPosition(x, y);

    while (window.isOpen())
    {
        sf::Event event;
        while (window.pollEvent(event))
        {
            if (event.type == sf::Event::Closed)
                window.close();
        }

        sf::Vector2f posicion = ball.getPosition(); // posición actual del círculo
        x = posicion.x;
        y = posicion.y;

        // r_ball*2 toma en cuenta el diámetro del círculo
        if (x == max_x - r_ball * 2 || x < min_x)
            movx *= -1;

        if (y == max_y - r_ball * 2 || y < min_y)
            movy *= -1;

        ball.move(movx, movy); // mueve el círculo de su posición actual
                               // de acuerdo a los valores de incremento movx y movy

        window.clear();
        window.draw(ball);
        window.display();

    } // window.isOpen

    return 0;
}
Las diferencias clave de la versión 2.0:
  • move(movx, movy) desplaza el objeto desde donde está, sin que tú calcules la suma — SFML lo hace por dentro.
  • getPosition() te devuelve la posición actual como un sf::Vector2f (un par x, y), así no necesitas variables externas para recordar dónde está la pelota.
  • El límite del rebote ahora es max_x - r_ball * 2: restamos el diámetro para que la pelota rebote cuando su borde toca la pared, no cuando su centro ya salió de la pantalla (un clásico bug de la versión 1.0, donde la pelota se "come" un poco el borde).
  • movx *= -1 es la forma corta de escribir movx = -movx: invierte el signo en una sola línea.

Siguiente lección

Comentarios

  1. Hace tiempo que buscaba este ejemplo. Gracias por compartir

    ResponderEliminar
  2. Muchas gracias, excelente aporte, un abrazo

    ResponderEliminar
  3. Respuestas
    1. Si te refieres a SFML, lo he podido probar en :

      codeblocks:
      https://intelligenciavirtual.blogspot.com/2014/09/instalar-codeblocks-en-windows.html

      Visual Studio:
      https://intelligenciavirtual.blogspot.com/2018/11/como-instalar-sfmf-25-en-visual-studio.html

      Linux:
      https://intelligenciavirtual.blogspot.com/2016/06/instalar-sfml-en-linux-debian.html



      Eliminar

Publicar un comentario

Entradas populares de este blog

Desarrollo de videojuegos paso a paso con C++ y SFML

Curso SFML C++: Dibujar un rectángulo con POO (RectangleShape paso a paso)