SDL e programação de jogos - melhor movimentação e separação da colisão - parte 4

Como vimos na última aula (SDL-e-programação-de-jogos-detecção-de-colisão) criamos uma função simples de detectar colisão entre dois retângulos SDL_Rect.

Neste novo tutorial, vamos ver os procedimentos para separar um retângulo de dentro do outro e também como melhorar a movimentação.

Sigam-me os maus porque deles serás o avião de batalha nos céus!

Melhorando a movimentação do player

Primeiro, pra poder separar os retângulos, devemos desacomplar ainda mais a lógica dos eventos da lógica de movimentação.

Veja no código da aula passada tem o seguinte código que faz mover o retângulo do player:


  //loop principal
  while (!fim) {
    //loop de eventos
    //novo loop de eventos
    while (SDL_PollEvent(&evento)) {
      //verifica se clicou em fechar
      if (evento.type == SDL_QUIT)
        fim = 1;
      //verifica se a tecla pra baixo foi apertada
      if (evento.type == SDL_KEYDOWN) {
        if (evento.key.keysym.sym == SDLK_DOWN) {
          //move o player pra baixo +5 pixels
          player.y = player.y + 5;
        }
        //verifica se a tecla apertada é a seta direita
        else if (evento.key.keysym.sym == SDLK_RIGHT) {
          //move o player +5 pixels pra direta
          player.x = player.x + 5;
        }
        //se é a tecla seta esquerda
        else if (evento.key.keysym.sym == SDLK_LEFT) {
          //mova pra esquerda, como que faz?
          //se somar +5 ao player.x é mover pra direita, então diminiuir -5 em player.x é mover pra esquerda!
          //R:
          player.x = player.x - 5;
        }
        else if (evento.key.keysym.sym == SDLK_UP) {
          //mmova pra cima, como que faz?
          //se mover pra baixo é somar +5 ao player.y, então, mover pra cima é diminuir -5 no player.y!
          //R:
          player.y = player.y - 5;
        }
      }
    }
    .. resto do loop principal
    //código do update
  }
Veja que, assim que é detectado o apertar de uma das quatro setas, o if da seta move imediatamente o player. No caso, ele soma ou subtrai 5 pixels da posição X ou Y do retângulo.

E veja também que o loop de eventos NÃO era para mover o player, sabe por quê? Porque o loop de eventos é pra ler o estado das entradas (teclado, mouse, monitor, etc) e somente FORA dele, ali onde tem o comentário com a palavra "update", é neste ponto que se deve realmente mover o player. Mover eu quero dizer adicionar ou subtrair 5 pixels da posição X ou Y do player.

Mas vc pode pensar: mas como caralhos vou mover o player FORA do loop de eventos se somente quando uma seta é apertada é que ele move?

Muito simples meu raparigo desconhecedor dos paranauês programáticos: vc deve usar variáveis para sinalizar FORA do loop de eventos quando o estado de uma tecla mudar!

Veja o algoritmo básico:
  1. Vc deve criar 4 variáveis ANTES do loop principal, uma para cada seta do teclado, quatro variáveis do tipo int (chame de setaEsquerda, setaDireita, setaCima, setaBaixo)
  2. Vc deve iniciar cada variável com um valor (vamos escolher igual a 0 como tecla não pressionada e valor 1 para tecla pressionada)
  3. Depois, dentro do loop de eventos, ali onde tem if (evento.type == SDL_KEYDOWN), vc deve trocar todo o código de mover, por cada variável da seta e definir o valor de acordo com o evento. Exemplo, se é o if (evento.key.keysym.sym == SDLK_RIGHT) e o comando do if é player.x = player.x + 5, então, vc apaga o comando dentro do if e troca ele pela variável setaDireita atribuindo o valor 1 (apertada) pra ela. Ou seja, vc faz setaDireita = 1;
  4. Ainda dentro do loop, vc deve adicionar um novo evento que é o if (evento.type == SDL_KEYUP) pra verificar se uma tecla foi solta. No código anterior, o evento SDL_KEYDOWN era repetido enquanto a tecla estava sendo segurada, mas aqui não podemos mais usar o mesmo esquema porque o código de mover fica todo FORA do loop de eventos.
  5. E dentro do novo evento o SDL_KEYUP vc deve adicionar quatro if, um pra cada seta, pra verificar qual tecla foi solta e depois, dentro de cada if, vc define o valor da variável correspondente a seta para o valor igual a 0. Ou seja, Se a tecla solta foi SDLK_RIGHT (seta direita), então, vc coloca no bloco do if o comando setaDireita = 0 para fazer o valor da setaDireita ser igual a 0 e indicar fora do loop de eventos que a seta direita está solta
  6. E ai, fora do loop de eventos e DENTRO do loop principal, na parte do update, vc verifica se o valor das quatro setas é igual a 1, se for igual a 1, então, vc move na direção da seta porque o valor igual a 1 é o mesmo que seta pressionada, e se o valor é igual a 0, vc não faz nada (não soma nem subtrai 5 da posição X e Y). Exemplo: vc faz pra cada variável seta if (setaDireita == 1) player.x = player.x + 5;
  7. E ai vc compila e testa pra ver como a magia acontece! It's a kind of magic... a kind of magic!
Falar é fácil, me mostre o código! Segue parte do código pra vc fazer o resto:

NOTA o código abaixo é pra ser modificado no último exemplo do tutorial anterior, veja ele aqui

//antes do loop principal
//Vou fazer duas setas de exemplo, é mamão com açúcar, melzinho na chupeta!
//define os valores das variáveis que representam as setas
int setaEsquerda = 0, setaDireita =0, setaCima = 0, setaBaixo = 0;
int fim = 0;
//loop principal
while (fim == 0) {
  //loop de eventos
  while (SDL_PollEvent(&evento)) {
    //... resto código
    //verifica se apertou uma tecla
    //este é o passo 3
    if (evento.type == SDL_KEYDOWN) {
      //se apertou seta direita do teclado
      if (evento.key.keysym.sym == SDLK_RIGHT) {
        //então, coloca como 1 o valor da variável que representa a seta direita
        setaDireita = 1;
      }
      //faça a seta cima aqui, tem código nooutro tutorial
      //só definir o valor de setaCima = 1;
    }
    //se soltou uma tecla
    //este é passo  4
    else if (evento.type == SDL_KEYUP) {
      //este é o passo 5
      //se a tecla solta é a direita
      if (evento.key.keysym.sym == SDLK_RIGHT) {
        //então, desaperte a set direita colocando nela o valor 0
        setaDireita = 0;
      }
      //faça a seta cima aqui, tem código no mesmo tutorial passado
      //só que no if da setaCIma defina o valor dela como setaCima = 0;
    }
    //... resto código
  }
//fora do loop de eventos
//...
//código de mover
//mover no eixo X (horizontal)
//se é pra mover pra direita
if (setaDireita == 1) {
  //faz mover pra direita
  player.x = player.x + 5;
}
//senão se setaDireita == 0, não faz nada
//se é pra mover pra esquerda
if (setaEsquerda == 1) {
  //faça o código de mover aqui
}
//senão se setaEsquerda == 0, não faz nada

//mover no eixo Y (vertical)
//se é pra mover pra baixo
if (setaBaixo == 1) {
  //faça o código de mover aqui
}
//senão se setaBaixo == 0, não faz nada
//se é pra mover pra cima
if (setaCima == 1) {
  //faz mover pra cima
  player.y = player.y - 5;
}
//senão se setaCima == 0, não faz nada
//...resto co do código
NOTAS:
  • Este código acima é pra ser usado no exemplo da aula passada. Baixe ele no link SDL-e-programação de jogos parte 3
  • Salve como move_e_separa_1.c e compile com: gcc -o move_e_separa_1 move_e_separa_1.c -lSDL2
Com essa lógica, o código dos eventos fica totalmente separado do código da movimentação. Ou seja, pra mover usamos o valor de variáveis declaradas externamente ao loop principal para então modificar os valores delas DENTRO do loop de eventos e ai fora dele, dentro do loop principal, ler o valor de cada variável das setas e então decidir o que fazer baseado nos valores atuais delas. Esse é o mais básico desacoplamento de lógica é ultra eficaz!

Baixe o exemplo completo aqui:

Processamento de colisão: Separação de dois retângulos colidindo

Vc viu que no exemplo anterior, a gente desacoplocou a lógica de movimentação de dentro do loop de eventos, mas não separamos os dois retângulos e vc pergunta: mas mestre, e como fazer isso? E o guru que comeu seu c* vai lhe dizer como ele faz isso!

Mas primeiro, vc precisa entender a teoria e ainda também precisa de um pouco mais de ajustes no código de mover (o ajuste é mínimo!).

Teoria básica de separação de objetos colidindo

Em jogos, vemos uma simulação de como é matemáticamente uma colisão entre retãngulos e retângulos, e entre retângulos e círculos, entre círculos e círculos, etc. Ou seja, dependendo de qual é a forma geométrica da figura escolhida, a gente processa a colisão de acordo com o par de formas geométricas. Nesse caso, o código de separar um retângulo colidindo com um outro retângulo, é diferente do código de separar um retângulo colidindo com um círculo, por exemplo.

Então, cada forma colide com a outra de maneira diferente e pra isso, precisamos fazer um código diferente pra cada par de formas geométricas.

Em jogos, vc deve usar formas simples (retangulos, circulos, etc) para diminuir o uso do processador a cada loop. Porque obviamente, temos de processar tais coisas uma vez por loop e um jogo de 60 fps por exempo, executa a lógica pelo menos 1000s/60fps=13 milissegundos! Ou seja, o loop principal do jogo é executado sempre a cada 13 milissegundos, então em 1 segundo (1000 milissegundos) o loop principal executa 60 ciclos (fps = frames por segundo).

Por isso aquele seu joguinho que roda a 30 fps foi feito assim pra diminuir o uso de processamento porque é porcamente otimizado e nem sempre o dev tem culpa por a empresa pedir "agilidade" no desenvolvimento do game. Sendo assim, tem que meter o pau no dono da empresa!

Voltando: em jogos, um player é feito de pelo menos uma forma (vou chamar agora de shape) e esse shape é quem colide com o chão, com outo personagem, etc.

Veja abaixo uma imagem demonstrativa de como funciona:
  1. o retangulo vermelho, representa o shape sólido pelo qual o mario colide com os tiles (as células do mapa)
  2. o sprite é a imagem do mario que contém uma posição relativa ao shape sólido do corpo do mario
  3. a união entre sprite (imagem ou animação) e o shape sólido é o que dá a impressão do mario ter colisão.
Chega de conversa, hora do código!

Código de separação da colisão entre dois retângulos

Pra separar dois retângulos, obviamente vc precisa primeiro detectar se eles estão colidindo, e se estão, ai sim vc separa um de dentro do outro. Porém meu jovem amigo pagodeiro, funkeiro e programador, vc tem que dividir mais a lógica do programa para poder separar sem dificuldade.

Existem diversas formas de separar um objeto de dentro de outro, mas neste tutorial, eu vou escolher uma um pouco ineficiente mas fácil de entender.

No o algoritmo é o seguinte:

  1. primeiro move no eixo X e depois colide em X
    1. verifique se é pra mover o player no eixo X e mova ele se for pra mover
    2. agora, verifique se o player está colidindo com o retângulo solido usando boundingBox(&player &solido)
    3. Se está colidindo, então:
      1. se o CENTRO X do player está à DIREITA do CENTRO X do solido então, faça player.x = solido.x + solido.w + 1;
      2. senão se o CENTRO X do player está à ESQUERDA do CENTRO X do solido então, faça player.x = solido.x - player.w - 1;
    4. se não está colidindo, não mude nada
  2. segundo move no eixo Y e depois colide em Y
    1. verifique se é pra mover o player no eixo Y e mova ele em Y se for pra mover
    2. agora, verifique se o player está colidindo com o retângulo solido usando boundingBox(&player &solido)
    3. Se está colidindo, então:
      1. se o CENTRO Y do player está ACIMA do CENTRO Y do solido, então, faça player.y = solido.y - player.h - 1;
      2. senão se o CENTRO Y do player está ABAIXO do CENTRO Y do solido, então, faça player.y = solido.y + solido.h + 1;
Vc pode conferir o código completo aqui:

Move e separa parte 2

Veja o seguinte:

No algoritmo acima, eu coloquei pra usar duas vezes a função boundingBox(), uma pra cada eixo. Isso serve pra separar a lógica de separação por eixo. Em engines completas, a detecção de colisão é feita somente uma vez e ai a engine calcula onde vai estar o player. Porém, aqui por simplicidade e facilidade de entendimento, eu preferi usar duas vezes a boundingBox, uma por eixo, porque se por acaso o player colidir em X com o solido, ele será separado do solido e não vai mais colidir em Y no próximo if. Ou pior, ele pode mover em X e NÃO colidir com o solido em Y.

Veja ainda que é o retângulo solido que realmente move pra fora o retângulo player. Ou seja, é uma colisão entre um objeto estático (o solido) e um objeto dinâmico(o player).

Então o esquema básico é: primeiro vc move em um eixo e depois colide nesse eixo e ai separa os objetos que colidem nesse eixo. Isso é feito pra cada eixo que o jogo tem.

Com o código acima, vc já tem um esqueleto básico e ideias de como funciona o processamento de colisão.

No próximo artigo, veremos como criar um sistema de forças para melhorar ainda mais o movimento e com ele já vamos fazer um pequeno jogo simples de pular!

Aguarde e quem viver verá! Arrocha meu chará!

Comentários

Postagens mais visitadas deste blog

SDL e programação de jogos parte 1

SDL e programação de jogos - detecção de colisão - parte 3

SDL e programação de jogos parte 2