Programação de Jogos com SDL

Este é um tutorial 2 em 1, vamos programar passo a passo dois jogos. O primeiro jogo será um jogo de labirinto e o segundo um snake (jogo da cobrinha). Os jogos serão feitos usando linguagem C e a biblioteca SDL.

[ Hits: 25.664 ]

Por: Samuel Leonardo em 18/11/2013


Jogo do labirinto



Esse jogo, eu fiz em 2008 baseado em outro jogo feito pelo Thiago Negri (hunz). Agora atualizei todo o código para o tutorial.

O jogo funcionará da seguinte maneira:
  • O jogador começará numa posição dentro do mapa e usará as setas do teclado para mover-se.
  • No mapa, haverá 4 tipos de objetos: paredes, gramado, caminho e ponto final.
  • O jogador poderá passar sobre o caminho ou gramado. E não passará sobre as paredes.
  • O jogo terminará quando o jogador chegar ao ponto final do mapa.

Baixe as imagens abaixo, elas serão necessárias para o jogo:
Linux: Programação de Jogos com SDL   Linux: Programação de Jogos com SDL   Linux: Programação de Jogos com SDL   Linux: Programação de Jogos com SDL

Obs.: quando executar o jogo, as imagens devem estar na mesma pasta do executável para funcionar.

Arquivo labirinto.c:

#include <stdio.h>
#include <stdlib.h>
#include <SDL/SDL.h>

/* Definicoes, para facilitar o uso da funcao para desenhar o mapa */
#define PAREDE 1
#define VOCE   2 /* não usado */
#define GRAMA  3
#define SAIDA  4

/* Desfinicoes da configuracao do Video */
#define LARGURA 800 /* Numero total de colunas*largura_da_parede */
#define ALTURA  200 /* Numero total de linhas*altura_da_parede */
#define BPP       0 /* A Flag SDL_ANYFORMAT se encaregara da resolucao */


SDL_Surface * tela, * piso, * parede, * player, * final, * grama;

int coluna_atual = 1, linha_atual = 1, fim = 0;

/* O Mapa */
int mapa[10][40] = {
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,3,3,3,
1,3,3,3,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,1,1,3,3,3,
1,3,1,3,3,1,1,1,0,1,1,1,1,1,1,1,1,0,0,1,1,1,0,1,1,1,1,1,1,1,1,0,1,0,0,0,0,1,0,1,
1,3,3,1,3,3,3,3,0,1,1,0,0,0,0,0,0,1,1,1,1,1,0,1,3,3,1,1,1,1,1,0,1,1,1,1,0,1,0,1,
1,3,3,1,3,3,3,3,0,1,1,0,1,1,1,1,1,1,1,1,1,1,0,1,3,3,0,0,0,1,1,0,1,1,1,1,0,1,0,1,
1,3,3,3,3,1,3,3,0,1,1,0,1,1,1,1,1,1,0,1,1,1,0,1,3,3,1,1,0,1,1,0,0,0,0,0,0,1,0,1,
1,3,3,3,3,3,3,3,0,0,0,0,1,1,1,1,1,0,1,1,1,1,0,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,0,1,
1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,0,1,1,1,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4,1,1,1,1,1,1,1,1,1,1,1
};

/*==========================FUNCOES==========================*/
/* Funcao que controla o fps */

void controla_fps ( int tempo_inicial )
{
int fps = 1000/60; // converte 60 FPS para milissegundos
int tempo_agora = SDL_GetTicks() - tempo_inicial;

if(tempo_agora < fps)
SDL_Delay(fps - tempo_agora);
}

/* Funcao de inicializacao */
int carrega_imagens (  )
{
/* Carrega as Imagens */
parede = SDL_LoadBMP("parede_muro.bmp");
if (parede == NULL)
{
printf("Não carregou parede_muro.bmp\n");
return 0;
}

player = SDL_LoadBMP("gamer.bmp");
if (player == NULL)
{
printf("Não carregou gamer.bmp\n");
return 0;
}

final = SDL_LoadBMP("fim.bmp");
if (final == NULL)
{
printf("Não carregou fim.bmp\n");
return 0;
}

grama = SDL_LoadBMP("gramado.bmp");
if (grama == NULL)
{
printf("Não carregou gramado.bmp\n");
return 0;
}

return 1;
}

/* Funcao para desenhar o Mapa */
void desenha_mapa (  )
{
SDL_Rect destino;
int linha, coluna;

for (linha = 0; linha < 10; linha++)
{
destino.y = linha * 20;
for (coluna = 0; coluna < 40; coluna++)
{
destino.x = coluna * 20;
if (mapa[linha][coluna] == PAREDE)
{
/* pegue a imagem parede completa(NULL) e jogue na tela em destino */
SDL_BlitSurface(parede, NULL, tela, &destino);
}
else if (mapa[linha][coluna] == GRAMA)
{
/* pegue a imagem grama completa(NULL) e jogue na tela em destino */
SDL_BlitSurface(grama, NULL, tela, &destino);
}
else if (mapa[linha][coluna] == SAIDA)
{
/* pegue a imagem final completa(NULL) e jogue na tela em destino */
SDL_BlitSurface(final, NULL, tela, &destino);
}
}
}
}

void move_jogador ( SDL_Event event )
{
/*=============== Deslocamento do jogador =====================*/
switch (event.key.keysym.sym)
{
/* Na vertical */
case SDLK_UP:
/* Se o usuario aperta a seta para cima e linha_atual for maior que 0 ... */
if (linha_atual > 0) /* 0 = primeira linha */
{
/* ...subtraia 1 de linha_atual, ou seja, suba uma linha... */
linha_atual = linha_atual - 1;
/* ... e verifique a localizacao do jogador ...*/
if (mapa[linha_atual][coluna_atual] == PAREDE)
linha_atual = linha_atual + 1; /* ... Se for sobre a PAREDE volte para a posição anterior. */
}
break;

case SDLK_DOWN:
if (linha_atual < 9)
{
linha_atual = linha_atual + 1;
if (mapa[linha_atual][coluna_atual] == PAREDE)
linha_atual = linha_atual - 1;
}
break;

/* Na horizontal */
case SDLK_LEFT:
/* Se o usuario aperta a seta para esquerda e coluna_atual for maior que 0 ... */
if (coluna_atual > 0) /* 0 = primeira coluna */
{
/* ...subtraia 1 de coluna_atual, ou seja, recue uma coluna ... */
coluna_atual = coluna_atual - 1;
/* ... e verifique a localizacao do jogador ...*/
if (mapa[linha_atual][coluna_atual] == PAREDE)
coluna_atual = coluna_atual + 1; /* ... Se for sobre a PAREDE adicione 1 a coluna_atual. */
}
break;

case SDLK_RIGHT:
if (coluna_atual < 39)/* 39 = última coluna */
{
coluna_atual = coluna_atual + 1;
if (mapa[linha_atual][coluna_atual] == PAREDE)
coluna_atual = coluna_atual - 1;
}
break;

default:
break;
}
}


int main (  )
{
/*inicializando a SDL e verificando possiveis erros */
if(SDL_Init(SDL_INIT_VIDEO) != 0)
{
printf("Erro: %s\n", SDL_GetError());
exit(-1);
}

SDL_Rect destino; /* para blitar o jogador */
SDL_Event evento; /* para os eventos */

/* Carrega as imagens */
if (carrega_imagens() == 0) /* Se não carregou uma ou outra imagem */
{
return 1; /* encerre o programa */
}

/* Configura o Video */
tela = SDL_SetVideoMode(LARGURA, ALTURA, BPP, SDL_SWSURFACE | SDL_ANYFORMAT);
if(tela == NULL)
{
printf("Erro: %s\n", SDL_GetError());
return 1; /* encerre o programa */
}

int tempo_inicial;

/* Loop principal */
while (fim == 0) /* Enquanto NÃO for verdadeiro o fim */
{
/* Para a funcao controla_fps */
tempo_inicial = SDL_GetTicks();

/* Loop de eventos */
while(SDL_PollEvent(&evento))
{
if(evento.type == SDL_QUIT)
fim = 1;

/* move o jogador */
if (evento.type == SDL_KEYDOWN)
move_jogador(evento);
}

/* Verifica se o jogador chegou ao ponto final */
if (mapa[linha_atual][coluna_atual] == SAIDA)
{
printf("Chegou ao ponto final\n");
}

/* Pinta a tela inteira de branco antes de desenhar o mapa, esse branco eh o caminho */
SDL_FillRect(tela, NULL, SDL_MapRGB(tela->format, 255, 255, 255));
/* Desenha o mapa sobre a tela */
desenha_mapa();

/* blita o jogador na tela */
/* para o jogador: destino.x = coluna_atual*largura_da_imagem e destino.y = linha_atual*altura_da_imagem */

destino.x = coluna_atual * 20;
destino.y = linha_atual * 20;
/* pegue a imagem player completa(NULL) e jogue na tela em destino */
SDL_BlitSurface(player, NULL, tela, &destino);

SDL_UpdateRect(tela,0,0,0,0); /* Atualiza a tela inteira */
controla_fps(tempo_inicial); // controla o FPS
}

/* Finalizando o SDL */
SDL_Quit();
return 0;
}

Para compilar, execute:

gcc -o labirinto labirinto.c -lSDL

Controle de FPS

Para o controle do FPS, usei uma função específica, veja o código:

void controla_fps ( int tempo_inicial )
{
  int fps = 1000/60; // converte 60 FPS para milissegundos
  int tempo_agora = SDL_GetTicks() - tempo_inicial;
  if(tempo_agora < fps)
    SDL_Delay(fps - tempo_agora);
}

Ela recebe, como parâmetro, o tempo inicial lido no início da iteração do loop principal. SDL_GetTicks() retorna o tempo em milissegundos desde a inicialização do SDL.

Internamente, controla_fps calcula o tempo necessário para "esperar" para voltar a executar o programa. 1000/60 é o FPS convertido para milissegundos, 60 é o valor do FPS (Frames Per Second), quanto maior for o valor do FPS, menor será o tempo de espera.

Basicamente, é isso que ela faz. Espera um pequeno tempo, esse tempo deve variar mais ou menos um pouco. Se não fosse essa função, o programa executaria cada iteração do loop principal em diferentes intervalos de tempo.

Isso quer dizer que, se o programa rodasse numa máquina mais fraca, o loop levaria mais tempo para executar; se fosse numa máquina mais rápida, o programa rodaria muito rápido. Usando um controle de FPS, garantimos que o programa rodará por igual em diferentes máquinas. Essa função pode ser usada, sem muita modificação, em outros programas.

Se você quiser ver como um programa sem controle do FPS roda, compile e execute o código abaixo:

Arquivo demo_fps.c:

#include <SDL/SDL.h>

int main()
{
SDL_Init(SDL_INIT_VIDEO);
SDL_Surface * screen;
SDL_Event event;
SDL_Rect dest = {0,0,0,0};
int vel = 25;

screen = SDL_SetVideoMode(640, 480, 16, SDL_SWSURFACE);

int done = 0;
while (done == 0)
{
while (SDL_PollEvent(&event))
{
if (event.type == SDL_QUIT)
done = 1;
}
dest.x += vel;
dest.y = 240;
dest.w = 128;
dest.h = 32;

if (dest.x < 0)
vel = 25;
else if (dest.x + dest.w > screen->w)
vel = -25;

SDL_FillRect(screen, NULL, 0x0);
SDL_FillRect(screen, &dest, SDL_MapRGB(screen->format, 255, 255, 0));
SDL_UpdateRect(screen, 0,0,0,0);
SDL_Delay(60);
}
SDL_Quit();
return 0;
}

Para compilar:

gcc -o demo_fps demo_fps.c -lSDL

Ele mostra um retângulo indo e voltando na horizontal. Olhe atentamente para o retângulo movendo, não tire o olho dele. Em certos momentos, o retângulo move mais rápido e em outros, move mais devagar, isso acontece porque não há um controle de FPS dentro do loop principal.

Se você colocar a função controla_fps() e a variável tempo_inicial no programa, verá que a variação do movimento da imagem diminuirá consideravelmente, o que significa que o programa está rodando numa velocidade quase que constante.

Carregando as imagens:

int carrega_imagens (  )
{
/* Carrega as Imagens */
parede = SDL_LoadBMP("parede_muro.bmp");
if (parede == NULL)
{
printf("Não carregou parede_muro.bmp\n");
return 0;
}

player = SDL_LoadBMP("gamer.bmp");
if (player == NULL)
{
printf("Não carregou gamer.bmp\n");
return 0;
}

final = SDL_LoadBMP("fim.bmp");
if (final == NULL)
{
printf("Não carregou fim.bmp\n");
return 0;
}

grama = SDL_LoadBMP("gramado.bmp");
if (grama == NULL)
{
printf("Não carregou gramado.bmp\n");
return 0;
}

return 1;
}

As SDL_Surface, que são as imagens do jogo, são variáveis globais. Usei uma única função para carregar e verificar se carregou cada imagem. Ela retorna 0, caso alguma imagem não seja carregada e retorna 1, quando todas as imagens foram corretamente carregadas.

Blitagem do mapa

Parte mais complicada é a da blitagem do mapa.

Usei uma função para ficar responsável pela blitagem do mapa. Na blitagem, dependendo do valor da célula atual, cada imagem tem sua própria SDL_Surface. As imagens da parede, por exemplo, são uma, a do ponto final outra, por isso será preciso usar ifs para identificar a imagem para o valor indicado.

Veja abaixo a função desenha_mapa():

/* Função para desenhar o Mapa */
void desenha_mapa (  )
{
SDL_Rect destino;
int linha, coluna;

for (linha = 0; linha < 10; linha++)
{
destino.y = linha * 20;
for (coluna = 0; coluna < 40; coluna++)
{
destino.x = coluna * 20;
if (mapa[linha][coluna] == PAREDE)
{
/* pegue a imagem parede completa(NULL) e jogue na tela em destino */
SDL_BlitSurface(parede, NULL, tela, &destino);
}
else if (mapa[linha][coluna] == GRAMA)
{
/* pegue a imagem grama completa(NULL) e jogue na tela em destino */
SDL_BlitSurface(grama, NULL, tela, &destino);
}
else if (mapa[linha][coluna] == SAIDA)
{
/* pegue a imagem final completa(NULL) e jogue na tela em destino */
SDL_BlitSurface(final, NULL, tela, &destino);
}
}
}
}

A função percorre cada célula da matriz e verifica qual imagem blitar na tela, indicado pelo valor da célula atual. O destino de cada imagem depende do valor das variáveis linha e coluna.

Na tela cada, imagem está localizada de 20 em 20 pixels. Multiplicando o valor da coluna por 20 (tamanho da imagem), conseguimos o destino.x da imagem na tela, o mesmo vale para o destino.y, que é linha vezes 20.

Movimento do jogador

void move_jogador ( SDL_Event event )
{
/*============= Deslocamento do jogador ===================*/
switch (event.key.keysym.sym)
{
/* Na vertical */
case SDLK_UP:
/* Se o usuário aperta a seta para cima e linha_atual for maior que 0 ... */
if (linha_atual > 0) /* 0 = primeira linha */
{
/* ...subtraia 1 de linha_atual, ou seja, suba uma linha... */
linha_atual = linha_atual - 1;
/* ... e verifique a localização do jogador ...*/
if (mapa[linha_atual][coluna_atual] == PAREDE)
    linha_atual = linha_atual + 1; /* ... Se for sobre a PAREDE adicione 1 a linha_atual. */
}
break;

case SDLK_DOWN:
if (linha_atual < 9)
{
linha_atual = linha_atual + 1;
if (mapa[linha_atual][coluna_atual] == PAREDE)
linha_atual = linha_atual - 1;
}
break;

/* Na horizontal */
case SDLK_LEFT:
/* Se o usuário aperta a seta para esquerda e coluna_atual for maior que 0 ... */
if (coluna_atual > 0) /* 0 = primeira coluna */
{
/* ...subtraia 1 de coluna_atual, ou seja, recue uma coluna ... */
coluna_atual = coluna_atual - 1;
/* ... e verifique a localização do jogador ...*/
if (mapa[linha_atual][coluna_atual] == PAREDE)
      coluna_atual = coluna_atual + 1; /* ... Se for sobre a PAREDE adicione 1 a coluna_atual. */
}
break;

case SDLK_RIGHT:
if (coluna_atual < 39)
{
coluna_atual = coluna_atual + 1;
if (mapa[linha_atual][coluna_atual] == PAREDE)
      coluna_atual = coluna_atual - 1;
}
break;

default:
break;
}
}

O movimento do jogador é feito pelas setas do teclado. As setas para cima e para baixo ficam responsáveis pelo movimento vertical (no eixo Y), e as setas direita e esquerda pelo movimento horizontal (no eixo X). As variáveis linha_atual e coluna_atual indicam a posição do jogador dentro do mapa (matriz). O movimento é feito como se estivéssemos caminhando entre as linhas e colunas de uma matriz qualquer.

Mover para cima, é o mesmo que mover da linha atual para uma linha anterior (linha_atual menos 1):

O máximo que se pode subir nas linhas é até 0 (primeira linha).

Mover para baixo, é o mesmo que mover da linha atual para uma linha posterior (linha_atual mais 1):




O máximo que se pode descer nas linhas é até 9 (última linha).

Mover para esquerda, é o mesmo que mover uma coluna para esquerda (coluna_atual menos 1):

O máximo que se pode ir para esquerda nas colunas é até 0 (primeira coluna).

Mover para direita, é o mesmo que mover uma coluna para direita (coluna_atual mais 1):

O máximo que se pode ir para direita nas colunas, é até 39 (última coluna).

A colisão é feita verificando se no mapa[linha_atual][coluna_atual] tem uma PAREDE, ou seja, se é igual a 1. Toda vez que mover o jogador, seja no eixo X ou Y, e tiver uma PAREDE no mapa[linha_atual][coluna_atual], deve-se fazer o movimento inverso para retirar o jogador de cima da parede.

Isso depende do movimento feito para chegar lá, por exemplo, se mover para cima (linha_atual menos 1) e tiver uma parede no mapa[linha_atual][coluna_atual] o movimento para desfazer ir para cima é ir para baixo (linha_atual mais 1).

Se mover para direita e tiver uma parede, o movimento de desfazer ir para direita é ir para esquerda. Ou seja, no geral é se tiver uma parede volte para a posição antes de mover.

No loop principal

while (fim == 0) /* Enquanto NÃO for verdadeiro o fim */
{
/* Para a função controla_fps */
tempo_inicial = SDL_GetTicks();

/* Loop de eventos */
while(SDL_PollEvent(&evento))
{
if(evento.type == SDL_QUIT)
fim = 1;

/* move o jogador */
if (evento.type == SDL_KEYDOWN)
move_jogador(evento);
}

/* Verifica se o jogador chegou ao ponto final */
if (mapa[linha_atual][coluna_atual] == SAIDA)
{
printf("Chegou ao ponto final\n");
}

/* Pinta a tela inteira de branco antes de desenhar o mapa, esse branco eh o caminho */
SDL_FillRect(tela, NULL, SDL_MapRGB(tela->format, 255, 255, 255));
/* Desenha o mapa sobre a tela */
desenha_mapa();

/* blita o jogador na tela */
/* para o jogador: destino.x = coluna_atual*largura_da_imagem e destino.y = linha_atual*altura_da_imagem */

destino.x = coluna_atual * 20;
destino.y = linha_atual * 20;
/* pegue a imagem player completa(NULL) e jogue na tela em destino */
SDL_BlitSurface(player, NULL, tela, &destino);

SDL_UpdateRect(tela,0,0,0,0); /* Atualiza a tela inteira */
controla_fps(tempo_inicial); // controla o FPS
}

O destino X e Y da imagem do jogador é feito multiplicando a linha_atual e coluna_atual por 20 (tamanho da imagem).

if (mapa[linha_atual][coluna_atual] == SAIDA) :: Verifica se o jogador chegou ao final da fase. Com isso, poderíamos terminar o game (encerrar o loop) ou carregar uma nova fase. Por enquanto, apenas uma mensagem é mostrada no console.

O que poderia ser colocado no Game

Poderia ler o mapa a partir de um arquivo. O formato poderia ser assim:

111111101111100001111111111111111111111
111100000111001100111111000001111111111
111101110110011110111111011100001100411
110001110000111110001100011111001101111
111111111111111111110000111111110000111

Com cada caractere, sendo uma célula da matriz mapa. Obviamente, deve-se converter o caractere lido para o valor inteiro, basta fazer valor_do_caractere - '0'. Poderia ler o mapa de um arquivo assim que o jogador chegasse no ponto final, mas teria de definir outra posição para o jogador dentro do mapa.

Também, poderia colocar outros obstáculos, como portas que abrem ao passar sobre um botão no mapa ou obstáculos móveis, que vai e vem pelo mapa.

Bem, por enquanto, isso fica para você mesmo implementar. :)

Página anterior     Próxima página

Páginas do artigo
   1. Introdução
   2. Jogo do labirinto
   3. Jogo da cobrinha
Outros artigos deste autor

Desenhando um avatar do Tux no InkScape

Desenhando fácil um pinguim no Inkscape

Criatividade para TI parte 1

Algoritmo Antissocial - Recuperando o Controle da sua Mente

Dicas para aprender programação

Leitura recomendada

Tutorial SFML

GNA: um Coprocessador para Aceleração Neural

OneAPI: A plataforma da Intel para facilitar o desenvolvimento com chips Intel, AMD, ARM, NVIDIA POWER e FPGA

Ponteiros - Saindo de Pesadelos

Android NDK: Desmistificando o acesso a códigos nativos em C

  
Comentários
[1] Comentário enviado por danniel-lara em 18/11/2013 - 08:11h

Parabéns pelo Artigo muito bom

[2] Comentário enviado por removido em 18/11/2013 - 19:18h

muito bom o artigo
preciso usar o sdl e gostaria de saber se vc tem os comandos para setar diretamente os pixels na tela
valeu

[3] Comentário enviado por SamL em 18/11/2013 - 19:33h

Antes de acessar os pixels é preciso mudar as permissões de leitura/escrita na SDL_Surface, para isso use SDL_LockSurface e SDL_UnlockSurface.
Por exemplo:
SDL_Surface * surface; // uma surface

SDL_LockSurface(surface); // ativa a escrita direta nos pixels de surface

// agora aqui você faria alguma coisa com os pixels
faça algo com surface->pixels

// depois de feito deve-se usar unlocksurface
SDL_UnlockSurface(surface);

Tem outra função que manipula pixels que está na documentação do SDL:
http://sdl.beuc.net/sdl.wiki/Pixel_Access
Mas observe que ainda será preciso usar SDL_LockSurface e SDL_UnlockSurface para acessar os pixels com putpixel e getpixel.

[4] Comentário enviado por removido em 06/12/2013 - 14:37h

Parabéns cara,você foi genial,gostei muito do seu artigo.


Contribuir com comentário




Patrocínio

Site hospedado pelo provedor RedeHost.
Linux banner

Destaques

Artigos

Dicas

Tópicos

Top 10 do mês

Scripts