Um sprite é uma imagem estacionária, que quando trocada por outra imagem parecida em um pequeno intervalo de tempo, dá a ilusão de movimento.
No gif acima, pela taxa de atualização de cada frame da imagem, percebe-se nitidamente que são imagens estacionárias colocadas em sequência. Caso a taxa de atualização da imagem (FPS) aumente, dará a impressão de movimento, como no jogo original do mario.
Neste exemplo, iremos criar um programa que possui uma imagem de fundo, e um personagem correndo de um lado para o outro do cenário.
Antes de explicar o código, é importante saber como funciona uma sprite. Para isso você precisa saber que os sprites que compões uma animação normalmente não vêm em arquivos de imagens separados. Até poderiam, mas não vêm. Normalmente eles estão todos juntos em um arquivo de imagem só, chamado folha de sprites (spritesheet, do inglês). No nosso programa, iremos usar um fundo estacionário, e folha de sprite abaixo:
Note que a folha de sprite possui todas as posições necessárias para o ciclo completo da movimentação do personagem. O fundo está na cor rosa e é a cor que será ignorada pelo programa, como aquelas telas verdes do cinema.
Para realizar a movimentação, precisamos a cada intervalo de tempo definido, mostrar um pedaço diferente da folha de sprites, e é essa a lógica que será explicada neste código:
#include <allegro5/allegro.h> #include <allegro5/allegro_native_dialog.h> #include <allegro5/allegro_image.h> #define FPS 60.0 #define LARGURA_TELA 800 #define ALTURA_TELA 380 ALLEGRO_DISPLAY *janela = NULL; ALLEGRO_EVENT_QUEUE *fila_eventos = NULL; ALLEGRO_TIMER *timer = NULL; ALLEGRO_BITMAP *folha_sprite = NULL; ALLEGRO_BITMAP *fundo = NULL; void error_msg(char *text){ al_show_native_message_box(janela,"ERRO", "Ocorreu o seguinte erro e o programa sera finalizado:", text,NULL,ALLEGRO_MESSAGEBOX_ERROR); } int inicializar(){ if (!al_init()){ error_msg("Falha ao inicializar a Allegro"); return 0; } if (!al_init_image_addon()){ error_msg("Falha ao inicializar o addon de imagens"); return 0; } timer = al_create_timer(1.0 / FPS); if(!timer) { error_msg("Falha ao criar temporizador"); return 0; } janela = al_create_display(LARGURA_TELA, ALTURA_TELA); if(!janela) { error_msg("Falha ao criar janela"); al_destroy_timer(timer); return 0; } al_set_window_title(janela, "Sprites"); fila_eventos = al_create_event_queue(); if(!fila_eventos) { error_msg("Falha ao criar fila de eventos"); al_destroy_timer(timer); al_destroy_display(janela); return 0; } //carrega a folha de sprites na variavel folha_sprite = al_load_bitmap("run2.bmp"); if (!folha_sprite){ error_msg("Falha ao carregar sprites"); al_destroy_timer(timer); al_destroy_display(janela); al_destroy_event_queue(fila_eventos); return 0; } //usa a cor rosa como transparencia al_convert_mask_to_alpha(folha_sprite,al_map_rgb(255,0,255)); //carrega o fundo fundo = al_load_bitmap("background.png"); if (!fundo){ error_msg("Falha ao carregar fundo"); al_destroy_timer(timer); al_destroy_display(janela); al_destroy_event_queue(fila_eventos); al_destroy_bitmap(folha_sprite); return 0; } al_register_event_source(fila_eventos, al_get_display_event_source(janela)); al_register_event_source(fila_eventos, al_get_timer_event_source(timer)); al_start_timer(timer); return 1; } int main(void){ int desenha = 1; int sair = 0; //largura e altura de cada sprite dentro da folha int altura_sprite=140, largura_sprite=108; //quantos sprites tem em cada linha da folha, e a atualmente mostrada int colunas_folha=4, coluna_atual=0; //quantos sprites tem em cada coluna da folha, e a atualmente mostrada int linha_atual=0, linhas_folha=2; //posicoes X e Y da folha de sprites que serao mostradas na tela int regiao_x_folha=0, regiao_y_folha=0; //quantos frames devem se passar para atualizar para o proximo sprite int frames_sprite=6, cont_frames=0; //posicao X Y da janela em que sera mostrado o sprite int pos_x_sprite=50, pos_y_sprite=150; //velocidade X Y que o sprite ira se mover pela janela int vel_x_sprite=4, vel_y_sprite=0; if (!inicializar()){ return -1; } while(!sair){ ALLEGRO_EVENT evento; al_wait_for_event(fila_eventos, &evento); /* -- EVENTOS -- */ if(evento.type == ALLEGRO_EVENT_TIMER){ //a cada disparo do timer, incrementa cont_frames cont_frames++; //se alcancou a quantidade de frames que precisa passar para mudar para o proximo sprite if (cont_frames >= frames_sprite){ //reseta cont_frames cont_frames=0; //incrementa a coluna atual, para mostrar o proximo sprite coluna_atual++; //se coluna atual passou da ultima coluna if (coluna_atual >= colunas_folha){ //volta pra coluna inicial coluna_atual=0; //incrementa a linha, se passar da ultima, volta pra primeira linha_atual = (linha_atual+1) % linhas_folha; //calcula a posicao Y da folha que sera mostrada regiao_y_folha = linha_atual * altura_sprite; } //calcula a regiao X da folha que sera mostrada regiao_x_folha = coluna_atual * largura_sprite; } //se o sprite estiver perto da borda direita ou esquerda da tela if ( pos_x_sprite + largura_sprite > LARGURA_TELA - 20 || pos_x_sprite < 20 ){ //inverte o sentido da velocidade X, para andar no outro sentido vel_x_sprite = -vel_x_sprite; } //atualiza as posicoes X Y do sprite de acordo com a velocidade, positiva ou negativa pos_x_sprite += vel_x_sprite; pos_y_sprite += vel_y_sprite; desenha=1; } else if(evento.type == ALLEGRO_EVENT_DISPLAY_CLOSE) { sair = 1; } /* -- ATUALIZA TELA -- */ if(desenha && al_is_event_queue_empty(fila_eventos)) { //desenha o fundo na tela al_draw_bitmap_region(fundo,0,0,LARGURA_TELA,ALTURA_TELA,0,0,0); //velocidade positiva (movendo para direita) if (vel_x_sprite > 0) //desenha sprite na posicao X Y da janela, a partir da regiao X Y da folha al_draw_bitmap_region(folha_sprite, regiao_x_folha,regiao_y_folha, largura_sprite,altura_sprite, pos_x_sprite,pos_y_sprite,0); else //desenha sprite, igual acima, com a excecao que desenha a largura negativa, ou seja, espelhado horizontalmente al_draw_scaled_bitmap(folha_sprite, regiao_x_folha,regiao_y_folha, largura_sprite,altura_sprite, pos_x_sprite+largura_sprite,pos_y_sprite, -largura_sprite,altura_sprite,0); al_flip_display(); desenha = 0; } } al_destroy_bitmap(folha_sprite); al_destroy_bitmap(fundo); al_destroy_timer(timer); al_destroy_display(janela); al_destroy_event_queue(fila_eventos); return 0; }
Na linha 56 carregamos a folha de sprites em folha_sprite, e na linha 65 chamamos a função al_convert_mask_to_alpha() que recebe um bitmap e uma cor, e faz com que a cor passada por parâmetro seja considerada transparente dentro do bitmap. Isto faz com que a cor de fundo do arquivo (R:255, G:0, B:255) seja considerada como cor transparente. Caso o arquivo carregado já possua transparência (canal alpha), como alguns arquivos .png por exemplo, você não precisará usar este comando.
Logo no início da função main(), criamos as principais variáveis responsáveis pela lógica da animação:
89-102
Começando pelo mais básico, quando formos mostrar um sprite na tela, não podemos mostrar toda a folha, portanto precisamos saber a largura e altura de cada sprite. É importante que a folha seja criada de maneira que suas dimensões sejam constantes e existe simetria entre eles. Mas isto é tarefa do designer do jogo. As variáveis que representam estas dimensões são altura_sprite e largura_sprite (linha 90).
Precisamos também saber qual a região da folha que será impressa na tela para o sprite atual, pois para o primeiro sprite será a região inicial da folha, onde X==0 e Y==0, mas para o segundo sprite, deverá ser X==108 (largura do sprite) e Y==0. Estas variáveis são regiao_x_folha e regiao_y_folha (linha 96).
Como percebe-se, precisamos ir acrescendo a regiao_x_folha para cada novo sprite a ser mostrado. Mas quantas vezes poderei fazer isso, antes de ter que pular para a próxima linha da folha (e recomeçar da primeira coluna)? Por conta disso, as variáveis colunas_folha, linhas_folha, linha_atual e coluna_atual. Estas representam quantas colunas e linhas possui a folha, e qual linha e coluna estamos mostrando atualmente, respectivamente (linha 92 e linha 94).
Agora precisamos saber de quanto em quanto tempo trocaremos de um sprite para o outro. Como o tempo do jogo é medido em frames, as variáveis frames_sprite e cont_frames guardarão quantos frames precisam passar para que o sprite seja trocado, e quantos frames já se passaram desde a última troca, respectivamente (linha 98). Note que quanto menor o valor, de frames_sprite, mais "rápida" será a transição.
Bom, só resta agora saber onde que o sprite será posicionado. As variáveis pos_x_sprite e pos_y_sprite representam as coordenadas X e Y da tela, respectivamente, que o sprite será posicionado (linha 100), enquanto as variáveis vel_x_sprite e vel_y_sprite representam qual o acréscimo de posição nas coordenadas X e Y que o sprite sofrerá a cada frame (linha 102).
Caso haja a necessidade da criação de vários sprites diferentes no programa, recomendo a criação de uma struct de sprites, onde cada elemento desta struct vai possuir estas variáveis.
As linhas a seguir mostram a lógica que transcorre a cada frame:
113-145
Na linha 115 o cont_frames incrementa a cada vez que o timer disparar, e na linha 117 é verificado se passou uma quantidade de frames suficiente para trocar o sprite.
Em caso afirmativo, resetamos o cont_frames (linha 119) e incrementamos a coluna_atual indicando que o sprite a ser mostrado está na próxima coluna da folha (linha 121).
Na linha 123 testamos se a coluna atualmente mostrada alcançou o número de colunas da folha, e em caso afirmativo, voltamos para a coluna 0 (linha 125), pulamos para próxima linha aplicando o resto da divisão para que a linha volte a ser 0 quando passar da última linha (linha 127) e por fim, calculando a região Y da folha (linha 129), que é a linha*altura do sprite e a região X da folha (linha 132) que é a coluna*largura do sprite.
Até então atualizamos o sprite, e agora nos resta atualizar a posição que ele será mostrado na janela. Para isso, na linha 135 testamos se o sprite está perto de uma das bordas da janela. pos_x_sprite + largura_sprite calula onde está a borda da direita do sprite, e LARGURA_TELA - 20 é 20 pixels à direita da borda da janela. pos_x_sprite < 20 verifica se o sprite está a menos de 20 pixels da borda esquerda da janela. Caso o sprite esteja próximo a uma das duas bordas da janela, a velocidade X do sprite irá alterar o sentido (linha 137), ou seja, ele irá começar a andar no sentido inverso que andava antes se afastando da borda da janela.
Na linha 141 e linha 142 as posições X e Y do sprite que será mostrado é atualizada. Caso a velocidade seja positiva, a posição será acrescida, e caso seja negativa, decrescida.
Resta agora entender a parte que atualiza o conteúdo da janela:
151-174
Na linha 154 usarmos a função al_draw_bitmap_region() ao invés da conhecida al_draw_bitmap(). A diferença entre as duas é que a primeira mostra somente um pedaço do bitmap na tela. Ela recebe os segintes parâmetros:
Após mostrar a imagem de fundo, verificamos na linha 157 se a velocidade é positiva ou não. Fazemos isso pois caso seja negativa, teremos que mostrar o sprite espelhado.
Caso seja positiva, mostramos o conteúdo da folha de sprites na região que contém o sprite desejado:
159-162
Caso contrário, chamamos a função al_draw_scaled_bitmap() que serve para mostrar um bitmap com suas dimensões alteradas. Assim como o al_draw_bitmap_region(), ele mostra um pedaço específico do bitmap, porém note os dois parâmetros extras:
165-169
Note que no código usamos o valor negativo da largura do sprite para mostrá-lo na tela. Isto significa que o sprite será mostrado espelhado no eixo X. Para compensar o fato que sua largura se dará para esquerda e não para a direita, mandamos mostrar na tela pos_x_sprite+largura_sprite como sua posição X.