Tutorial de SDL – Parte 2 – Exibindo uma imagem na tela

Continuando o tutorial sobre SDL publicado originalmente no site lazyfoo, iremos ver como exibir uma imagem na tela.

Agora que vimos como criar uma janela, vamos exibir uma imagem nela.
Nota: De agora em diante cada artigo irá mostrar apenas partes do código donte. Para o código completo, você terá que baixar o código fonte no final do artigo.

//Starts up SDL and creates window
bool init();
//Loads media
bool loadMedia();
//Frees media and shuts down SDL
void close();

No primeiro artigo, colocamos tudo na função main. Como lá tinhamos um programa pequeno nós pudemos fazer isso, mas em programas reais (como videogames) você terá que criar seu código da forma mais modular possível. Isso significa que você irá organizar seu código em pedaços que sejam de fácil leitura e depuração.
No código acima, temos função para lidar com a inicialização, carregamento dos arquivos e encerramento da aplicação SDL. Declaramos essas funções no topo do arquivo.

//The window we'll be rendering to
SDL_Window* gWindow = NULL;
//The surface contained by the window
SDL_Surface* gScreenSurface = NULL;
//The image we will load and show on the screen
SDL_Surface* gHelloWorld = NULL;

Aqui declaramos algumas variáveis globais. Tipicamente, você deve evitar o uso de variáveis globais em programas grandes. A razão pela qual iremos usar aqui é por querermos que o código seja o mais simples possível, mas em projetos maiores variáveis globais tornam as coisas mais complicadas. Como este programa é composto de um único arquivo de código não precisamos nos preocupar muito com esse detalhe.
Temos aqui um novo tipo de dados chamado SDL_Surface, que é apenas um tipo de dados que armazena os pixels de uma imagem junto com os todos os dados necessários para renderiza-la. Esse tipo usa renderização por software, o que significa que usa a CPU para renderizar a imagem. É possível renderizar imagens pelo hardware, mas isso é um pouco mais difícil. Assim iremos aprender a forma mais fácil primeiro. Em artigo futuros iremos ver como renderizar imagens através de uma GPU.
As imagens que iremos usar aqui serão as imagens da tela (o que você vê dentro a janela) e a imagem que iremos carregar de um arquivo.
Observe que estamos usando ponteiros para SDL_Surfaces. O motivo para isso é que 1) iremos alocar dinamicamente a memória para carregar as imagens e 2) é melhor referenciar uma imagem por sua localização na memória. Imagine que voc~e tenha um jogo com uma parede de tijolos que consista da mesma imagem de um bloco renderizada múltiplas vezes (como Super Mario Bros). É um desperdício ter dezenas de cópias da imagem na memória quando você pode ter um única cópia da imagem e renderiza-la várias vezes.
Lembrando que você deve sempre inicializar seus ponteiros. Nós associamos o valor NULL à eles no momento que declaramos eles.

bool init()
{
 //Initialization flag
 bool success = true;
 //Initialize SDL
 if( SDL_Init( SDL_INIT_VIDEO ) < 0 )
 {
  printf( "SDL could not initialize! SDL_Error: %s\n", SDL_GetError() );
  success = false;
 }
 else
 {
  //Create window
  gWindow = SDL_CreateWindow( "SDL Tutorial", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, SCREEN_WIDTH, SCREEN_HEIGHT, SDL_WINDOW_SHOWN );
  if( gWindow == NULL )
  {
   printf( "Window could not be created! SDL_Error: %s\n", SDL_GetError() );
   success = false;
  }
  else
  {
   //Get window surface
   gScreenSurface = SDL_GetWindowSurface( gWindow );
  }
 }
 return success;
}

Como você pode ver acima, pegamos o código para a inicialização do SDL e a criação da janela e colocamos em sua própria função. A novidade aqui é a chamada para SDL_GetWindowSurface.
Como queremos exibir imagens dentro da janela precisamos obter essa imagem. Assim, chamamos SDL_GetWindowSurface para capturar a superfície contida na janela.

bool loadMedia()
{
 //Loading success flag
 bool success = true;
 //Load splash image
 gHelloWorld = SDL_LoadBMP( "02_getting_an_image_on_the_screen/hello_world.bmp" );
 if( gHelloWorld == NULL )
 {
  printf( "Unable to load image %s! SDL Error: %s\n", "02_getting_an_image_on_the_screen/hello_world.bmp", SDL_GetError() );
  success = false;
 }
 return success;
}

Nesta função, nós carregamos a nossa imagem usando a função SDL_LoadBMP. Essa função lê o caminho para um arquivo BMP e retorna a superfície carregada. Se a função retornar NULL, isso significa que ocorreu um erro e assim exibimos uma mensagem de erro no terminal usando a função SDL_GetError.
Uma coisa importante para observar aqui é que o código acima assume que você possui um diretório chamado “02_getting_an_image_on_the_screen” que contém uma imagem chamada “hello_world.bmp” em seu diretório de trabalho. O diretório de trabalho é o local onde sua aplicação acha que está sendo executada. Tipicamente, esse diretório é aquele onde está o arquivo executável da aplicação, mas programas como o Visual Studio consideram o diretório de trabalho o local onde está o arquivo vcxproj. Dessa forma, se sua aplicação não conseguir localizar a imagem, ponha ela no local certo.

void close()
{
 //Deallocate surface
 SDL_FreeSurface( gHelloWorld );
 gHelloWorld = NULL;
 //Destroy window
 SDL_DestroyWindow( gWindow );
 gWindow = NULL;
 //Quit SDL subsystems
 SDL_Quit();
}

Em nosso código de limpeza, destruímos a janela encerramos o SDL como fizemos anteriormente mas também cuidamos das superfícies que carregamos. Fazemos isso esvaziando elas com a função SDL_FreeSurface. Não se preocupem com a superfície da tela, SDL_DestroyWindow lida com ela.
Certifique-se de criar o hábito de alterar o valor de seu ponteiros para NULL quando eles não estiverem apontando para alguma coisa.

int main( int argc, char* args[] )
{
 //Start up SDL and create window
 if( !init() )
 {
  printf( "Failed to initialize!\n" );
 }
 else
 {
  //Load media
  if( !loadMedia() )
  {
   printf( "Failed to load media!\n" );
  }
  else
  {
   //Apply the image
   SDL_BlitSurface( gHelloWorld, NULL, gScreenSurface, NULL );

Em nossa função main nós inicializamos o SDL e carregamos a imagem. Se isso for bem sucedido, nós inserimos a superfície carregada na tela usando a função SDL_BlitSurface.
O que essa função faz é pegar a superfície original e por uma cópia dela sobre o destino. O primeiro argumento de SDL_BlitSurface é a imagem original. O terceiro argumento é o destino. Iremos falar sobre o segundo e quarto argumentos em artigo futuros.
Agora se esse fosse o único código de desenho que tivéssemos, ainda não veríamos  a imagem que carregamos na tela. Temos mais um passo.

//Update the surface
SDL_UpdateWindowSurface( gWindow );

Após desenhar tudo que queremos exibir na tela temos que atualiza-la usando a função SDL_UpdateWindowSurface. Veja bem, quando você desenha algo na tela, você tipicamente não está desenhando a imagem na tela que você está visualizando. Por padrão, a maioria dos sistemas de renderização possuem buffers duplos. Esses dois buffers são o frontal e o traseiro.
Quando você chama funções como a SDL_BlitSurface você está renderizando no buffer traseiro. O que você visualiza na tela é o buffer frontal. A razão disso é por que a maioria dos frames precisam desenhar múltiplos objetos na tela. Se tivéssemos apenas o buffer frontal, veríamos o frame a medida que as coisas estivessem sendo desenhadas nele, o que significa que veríamos frames não finalizados. Assim, o que fazemos é desenhar tudo no buffer traseiro primeiro e quando tudo estiver feito trocamos entre ele o frontal de forma que o frame finalizado possa ser exibido.
Isso também significa que você não chama SDL_UpdateWindowSurface depois de cada chama à SDL_BlitSurface, apenas depois que todas as inserções no frame atual tiverem sido feitas.

  //Wait two seconds
  SDL_Delay( 2000 );
 }
}
 //Free resources and close SDL
 close();
 return 0;
}

Agora que renderizamos todos os elementos da janela, aguardamos por dois segundos antes da janela sumir. Depois de tudo, encerramos nosso programa.
Baixe o arquivo de imagem e o código fonte desse artigo aqui.