Buffer de color¶
El buffer de color es un array de colores (en la práctica uint32_t
) que maneja cada uno de los píxeles de la pantalla y su color.
La variable que gestiona este buffer es el puntero color_buffer
, donde se reserva memoria dinámicamente con malloc
. El tamaño es el número de píxeles de la pantalla (ancho * alto):
uint32_t *color_buffer;
color_buffer = static_cast<uint32_t *>(malloc(sizeof(uint32_t) * window_width * window_height));
Para dibujar ese color_buffer
se tiene que copiar a una textura color_buffer_texture
:
SDL_Texture *color_buffer_texture;
color_buffer_texture = SDL_CreateTexture( renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, window_width, window_height);
El proceso de copia del color_buffer
a la textura y al renderer ocurrirá en cada fotograma:
void render_color_buffer()
{
SDL_UpdateTexture(color_buffer_texture, NULL, color_buffer, window_width * sizeof(uint32_t));
SDL_RenderCopy(renderer, color_buffer_texture, NULL, NULL);
}
El proceso de renderizado empieza con un color de base para limpiar la pantalla, luego una limpieza y renderizado del color_buffer
y finalmente la actualización de la pantalla:
void render()
{
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
SDL_RenderClear(renderer);
clear_color_buffer(0xFFF00FFFF);
render_color_buffer();
SDL_RenderPresent(renderer);
}
La limpieza del color_buffer
se basa en recorrer todos los píxeles del array y establecer el color pasado a la función:
void clear_color_buffer(uint32_t color)
{
for (size_t y = 0; y < window_height; y++)
{
for (size_t x = 0; x < window_width; x++)
{
color_buffer[(window_width * y) + x] = color;
}
}
}
En este punto podemos crear diferentes funciones para cambiar dinámicamente los colores del color_buffer
, por ejemplo para generar una cuadrícula draw_grid()
:
void draw_grid(uint32_t color)
{
for (size_t y = 0; y < window_height; y += 10)
{
for (size_t x = 0; x < window_width; x += 10)
{
color_buffer[(window_width * y) + x] = color;
}
}
}
O una función para dibujar rectángulos rellenos de colores draw_rectangle()
:
void draw_rect(int sx, int sy, int width, int height, uint32_t color)
{
for (size_t y = sy; (y < sy + height) && (y < window_height); y++)
{
for (size_t x = sx; (x < sx + width) && (x < window_width); x++)
{
color_buffer[(window_width * y) + x] = color;
}
}
}
Dibujar FPS y caparlos¶
He decidido añadir una opción para dibujar la media de FPS. Para ello necesitaré dibujar un texto en pantalla con la biblioteca SDL_ttf.
La versión utilizada en el proyecto es SDL2_ttf-devel-2.0.18 x86_64-w64-mingw32, cuyos include
y lib
van al directorio src
y la DLL SDL2_ttf.dll
al directorio bin
.
Una vez hecho se puede importar para utilizarlo:
#include <SDL2/SDL.h>
#include <SDL2/SDL_ttf.h>
Para dibujar el texto hay que seguir varios pasos.
En primer lugar se necesita una superficie para renderizar el texto, un color y una fuente que deberemos cargar luego:
SDL_Surface *text;
TTF_Font *font;
SDL_Color color = {255, 255, 255};
Deberemos inicializar el módulo TTF:
if (TTF_Init() < 0)
{
std::cout << "Error initializing SDL_ttf: " << TTF_GetError() << std::endl;
return false;
}
También durante la inicialización si todo es correcto cargaremos la configuración de la fuente que debemos tener en algún directorio del proyecto:
// font setup
font = TTF_OpenFont("assets/FreeSans.ttf", 16);
if (!font)
{
std::cout << "Error loading font: " << TTF_GetError() << std::endl;
return false;
}
Durante el renderizado la idea es renderizar el texto deseado en la superficie:
text = TTF_RenderText_Solid(font, "Hola mundo!", color);
if (!text)
{
std::cout << "Failed to render text: " << TTF_GetError() << std::endl;
}
Justo a continuación renderizaremos la superficie como una textura, configuraremos el tamaño de la recta ed destino y realizaremos la copia al renderer
:
SDL_Texture *text_texture = SDL_CreateTextureFromSurface(renderer, text);
SDL_Rect dest = {2, 459, text->w, text->h};
SDL_RenderCopy(renderer, text_texture, NULL, &dest);
Después de actualizar la pantalla podemos liberar de la memoria la textura y la superficie (es muy importante):
// Liberación de memoria local
SDL_DestroyTexture(text_texture);
SDL_FreeSurface(textSurface);
También deberemos liberar la memoria de la fuente y el módulo TTF al destruir la ventana:
TTF_CloseFont(font);
TTF_Quit();
Con esto ya tendremos nuestro texto en pantalla:
Ahora debemos idear una forma de limitar los FPS en caso de que no queramos tener la sincronización vertical activa:
renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
Siguiendo la idea del tutorial de LazyFoo he decidido crear mis propia clase Timer
para tener unos objetos más sofisticados.
La cabecera timer.h
contiene las definiciones:
#ifndef TIMER_H
#define TIMER_H
#include <iostream>
#include <SDL2/SDL.h>
// The application time based timer
class Timer
{
public:
// Initializes variables
Timer();
// The various clock actions
void start();
void stop();
void pause();
void unpause();
// Gets the timer's time
uint32_t getTicks();
// Checks the status of the timer
bool isStarted();
bool isPaused();
private:
// The clock time when the timer started
uint32_t mStartTicks;
// The ticks stored when the timer was paused
uint32_t mPausedTicks;
// The timer status
bool mPaused;
bool mStarted;
};
#endif
Y el fichero de fuentes timer.cpp
la implementación de la clase:
#include "timer.h"
Timer::Timer()
{
// Initialize the variables
mStartTicks = 0;
mPausedTicks = 0;
mPaused = false;
mStarted = false;
}
void Timer::start()
{
// Start the timer
mStarted = true;
// Unpause the timer
mPaused = false;
// Get the current clock time
mStartTicks = SDL_GetTicks();
mPausedTicks = 0;
}
void Timer::stop()
{
// Stop the timer
mStarted = false;
// Unpause the timer
mPaused = false;
// Clear tick variables
mStartTicks = 0;
mPausedTicks = 0;
}
void Timer::pause()
{
// If the timer is running and isn't already paused
if (mStarted && !mPaused)
{
// Pause the timer
mPaused = true;
// Calculate the paused ticks
mPausedTicks = SDL_GetTicks() - mStartTicks;
mStartTicks = 0;
}
}
void Timer::unpause()
{
// If the timer is running and paused
if (mStarted && mPaused)
{
// Unpause the timer
mPaused = false;
// Reset the starting ticks
mStartTicks = SDL_GetTicks() - mPausedTicks;
// Reset the paused ticks
mPausedTicks = 0;
}
}
uint32_t Timer::getTicks()
{
// The actual timer time
uint32_t time = 0;
// If the timer is running
if (mStarted)
{
// If the timer is paused
if (mPaused)
{
// Return the number of ticks when the timer was paused
time = mPausedTicks;
}
else
{
// Return the current time minus the start time
time = SDL_GetTicks() - mStartTicks;
}
}
return time;
}
bool Timer::isStarted()
{
// Timer is running and paused or unpaused
return mStarted;
}
bool Timer::isPaused()
{
// Timer is running and paused
return mPaused && mStarted;
}
Esta clase abastrae la función de SDL SDL_GetTicks()
y la maneja internamente además de proveer de métodos para manejar el temporizador.
La idea para calcular los FPS es la siguiente, antes de empezar el bucle de juego creamos un temporizador y lo iniciamos:
Timer fpsTimer;
fpsTimer.start();
También definiremos una variable para contar los fotogramas actuales:
int countedFrames = 0;
En cada iteración calcularemos los fps de media a partir del valor actual del contador entre el temporizador entre 1000, luego incrementaremos los fotogramas. Estos fps de media avgFPS
los tendremos como una variable global para renderizar ese valor en el texto de antes:
float avgFPS = 0; // <--- global
//...
avgFPS = countedFrames / (fpsTimer.getTicks() / 1000.f);
++countedFrames
Para dibujar los FPS modificaremos el renderizado de texto:
text = TTF_RenderText_Solid(font, (std::to_string(avgFPS) + " fps").c_str(), color);
Por defecto al tener la sincronización activada los FPS se autoregulan a los hercios de mi pantalla, que son 144:
Ahora nos falta añadir la limitación de FPS como alternativa a desactivar la sincronización vertical, esto se denomina el fps cap.
Para ello definiremos unas variables globales que manejarán si activamos o no el cap, la limitación de FPS deseada y el cálculo de ticks de pantalla por fotograma que utilizará nuestro futuro capTimer
:
bool enableCap = true;
int fpsCap = 60;
int screenTicksPerFrame = 1000 / fpsCap;
Procedemos a definir el capTimer
antes del while
:
Timer capTimer;
Lo iniciaremos justo al comenzar la iteración del while
:
while (is_running){
capTimer.start();
Finalmente, después de renderizar la pantalla al final del while
, realizaremos un ajuste para retrasar la siguiente iteración (utilizando SDL_Delay
) en función de los ticks de pantalla que resten para llegar a los screenTicksPerFrame
calculados antes:
// Si el fotograma finaliza demasiado pronto
int frameTicks = capTimer.getTicks();
if (enableCap && frameTicks < screenTicksPerFrame)
{
// Esperamos el tiempo restante
SDL_Delay(screenTicksPerFrame - frameTicks);
}
Con esto habremos logrado un límite de FPS manual en caso de no querer la sincronización vertical o podemos desactivarlo utilizando enableCap
:
bool enableCap = true;
Aquí se aprecia el cap manual a 60FPS:
Refactorización 1¶
Antes de continuar con el siguiente tema sobre vectores y puntos voy a reorganizar los ficheros del proyecto en clases para que todo sea más cómodo de utilizar.
Esencialmente voy a abstraer todo el proceso de gestión de la ventana y renderizado en una clase Window
.
Las cabeceras window.h
por ahora quedarán de la siguiente forma, también después de cambiar la notación a PascalCase en los métodos y camelCase en las variables:
#ifndef WINDOW_H
#define WINDOW_H
#include <iostream>
#include <SDL2/SDL.h>
#include <SDL2/SDL_ttf.h>
#include "timer.h"
class Window
{
public:
bool running = false;
int windowWidth;
int windowHeight;
private:
/* Window */
bool isFullscreen = false;
SDL_Window *window;
SDL_Renderer *renderer;
/* Color buffer */
uint32_t *colorBuffer;
SDL_Texture *colorBufferTexture;
/* Text */
SDL_Surface *textSurface;
SDL_Color textColor = {255, 255, 255};
TTF_Font *textFont;
/* Fps */
float avgFPS = 0;
bool enableCap = true;
int fpsCap = 60;
int screenTicksPerFrame = 1000 / fpsCap;
long countedFrames = 0;
/* Timers */
Timer fpsTimer, capTimer;
public:
Window(int w, int h) : windowWidth(w), windowHeight(h){};
~Window();
void Init();
void Setup();
void ProcessInput();
void Update();
void Render();
void PostRender();
void ClearColorBuffer(uint32_t color);
void RenderColorBuffer();
void DrawGrid(unsigned int color);
void DrawRect(int sx, int sy, int width, int height, uint32_t color);
};
#endif
En cuanto al código fuente window.cpp
la implementción completa es:
#include "window.h"
Window::~Window()
{
std::cout << "Destroying Window";
// Liberar la memoria dinámica
free(colorBuffer);
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
TTF_CloseFont(textFont);
TTF_Quit();
SDL_Quit();
}
void Window::Init()
{
running = true;
// Inicializamos SDL
if (SDL_Init(SDL_INIT_EVERYTHING) != 0)
{
std::cout << "Error initializing SDL." << std::endl;
running = false;
}
// Inicialización TTF
if (TTF_Init() < 0)
{
std::cout << "Error initializing SDL_ttf: " << TTF_GetError() << std::endl;
running = false;
}
// Utilizar SDL para preguntar la resolucion maxima del monitor
SDL_DisplayMode Window_mode;
SDL_GetCurrentDisplayMode(0, &Window_mode);
if (isFullscreen)
{
windowWidth = Window_mode.w;
windowHeight = Window_mode.h;
}
// Creamos la ventana SDL
window = SDL_CreateWindow(
NULL, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
windowWidth, windowHeight, SDL_WINDOW_BORDERLESS);
if (!window)
{
std::cout << "Error creating SDL Window." << std::endl;
running = false;
}
// Creamos el renderizador SDL
if (enableCap)
{
renderer = SDL_CreateRenderer(window, -1, 0);
}
else
{
renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
}
if (!renderer)
{
std::cout << "Error creating SDL renderer." << std::endl;
running = false;
}
// font setup
textFont = TTF_OpenFont("assets/FreeSans.ttf", 16);
if (!textFont)
{
std::cout << "Error loading font: " << TTF_GetError() << std::endl;
running = false;
}
if (isFullscreen)
{
SDL_SetWindowFullscreen(window, SDL_WINDOW_FULLSCREEN);
}
if (!running)
{
std::cout << "Window Init Fail";
}
}
void Window::Setup()
{
// Reservar la memoria requerida en bytes para mantener el color buffer
colorBuffer = static_cast<uint32_t *>(malloc(sizeof(uint32_t) * windowWidth * windowHeight));
// Crear la textura SDL utilizada para mostrar el color buffer
colorBufferTexture = SDL_CreateTexture(
renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, windowWidth, windowHeight);
// Start Timers
fpsTimer.start();
capTimer.start();
}
void Window::ProcessInput()
{
SDL_Event event;
SDL_PollEvent(&event);
switch (event.type)
{
case SDL_QUIT:
running = false;
break;
case SDL_KEYDOWN:
if (event.key.keysym.sym == SDLK_ESCAPE)
running = false;
break;
}
}
void Window::Update()
{
if (enableCap)
{
// Iniciar el temporizador de cap
capTimer.start();
}
// Calculate fps
avgFPS = countedFrames / (fpsTimer.getTicks() / 1000.f);
// Increment the frame counter
++countedFrames;
}
void Window::Render()
{
// Limpiar el color buffer
ClearColorBuffer(static_cast<uint32_t>(0xFF0000000));
}
void Window::PostRender()
{
// Renderizar el color buffer
RenderColorBuffer();
// Render FPS
textSurface = TTF_RenderText_Solid(textFont, (std::to_string(avgFPS) + " fps").c_str(), textColor);
if (!textSurface)
{
std::cout << "Failed to render text: " << TTF_GetError() << std::endl;
}
SDL_Texture *textTexture = SDL_CreateTextureFromSurface(renderer, textSurface);
SDL_Rect dest = {2, windowHeight - 21, textSurface->w, textSurface->h};
SDL_RenderCopy(renderer, textTexture, NULL, &dest);
// Liberación de memoria local
SDL_FreeSurface(textSurface);
SDL_DestroyTexture(textTexture);
// Finalmente actualizar la pantalla
SDL_RenderPresent(renderer);
// Y por último capear los fotogramas si es necesario
if (enableCap)
{
int frameTicks = capTimer.getTicks();
if (frameTicks < screenTicksPerFrame)
{
// Esperamos el tiempo restante
SDL_Delay(screenTicksPerFrame - frameTicks);
}
}
}
void Window::ClearColorBuffer(uint32_t color)
{
for (size_t y = 0; y < windowHeight; y++)
{
for (size_t x = 0; x < windowWidth; x++)
{
colorBuffer[(windowWidth * y) + x] = color;
}
}
}
void Window::RenderColorBuffer()
{
// Copiar el color buffer y su contenido a la textura
// Así podremos dibujar la textura en el renderer
SDL_UpdateTexture(colorBufferTexture, NULL, colorBuffer, windowWidth * sizeof(uint32_t));
SDL_RenderCopy(renderer, colorBufferTexture, NULL, NULL);
}
void Window::DrawGrid(unsigned int color)
{
for (size_t y = 0; y < windowHeight; y += 10)
{
for (size_t x = 0; x < windowWidth; x += 10)
{
colorBuffer[(windowWidth * y) + x] = static_cast<uint32_t>(color);
}
}
}
void Window::DrawRect(int sx, int sy, int width, int height, uint32_t color)
{
for (size_t y = sy; (y < sy + height) && (y < windowHeight); y++)
{
for (size_t x = sx; (x < sx + width) && (x < windowWidth); x++)
{
colorBuffer[(windowWidth * y) + x] = static_cast<uint32_t>(color);
}
}
}
Inicializar y empezar a trabajar con la ventana es ahora muy sencillo, así quedará main.cpp
:
#include <iostream>
#include "window.h"
int main(int argc, char *argv[])
{
Window window(640, 480);
window.Init();
window.Setup();
while (window.running)
{
window.ProcessInput();
window.Update();
window.Render();
window.DrawGrid(0xFF616161);
window.DrawRect(50, 50, 100, 100, 0xFF1570E8);
window.DrawRect(205, 125, 300, 200, 0xFFD93E23);
window.DrawRect(375, 225, 300, 300, 0xFFE35FDA);
window.PostRender();
}
return 0;
}
A comentar la variable pública window.running
que permite saber en todo momento si la ventana está funcionando para seguir ejecutando el bucle.
Luego los distintos métodos de mismo nombre ProcessInput
, Update
, Render
y un nuevo PostRender
que me permite separar el renderizado en dos partes y dibujar entre tanto diferentes elementos y por encima dibujar los FPS y presentar el renderer
.
Por cierto, tampoco necesitamos limpiar la pantalla en el renderer, pues estamos dibujando directamente nuestro colorBuffer
:
void Window::Render()
{
// Establecer el color del renderizador
// SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
// Limpiar la pantalla con el color establecido
// SDL_RenderClear(renderer);
// Limpiar el color buffer
ClearColorBuffer(static_cast<uint32_t>(0xFF0000000));
}
BUG: Congelamiento de Timer al mover ventana
Cuando se hace clic en una ventana SDL con los bordes activados:
window = SDL_CreateWindow(
NULL, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
windowWidth, windowHeight, 0); // SDL_WINDOW_BORDERLESS
La ejecución del código se bloqueará porque SDL_PollEvent(&event)
queda parado a la espera del siguiente evento. Sin embargo los timers 'SDL_GetTicks()` siguen funcionando y eso ocasionará un retraso en al contabilidad de fotogramas.
Para evitar este problema simplemente debemos pausar y continuar el fpsTimer
antes y después de recibir el siguiente evento:
void Window::ProcessInput()
{
// Pausar timer para prevenir congelamiento
fpsTimer.pause();
// Esperar y guardar el siguiente evento SDL
SDL_PollEvent(&event);
// Continuar al recibir cualquier evento
fpsTimer.unpause();
Última edición: 05 de Junio de 2022