Tutorial: Criando um jogo ao estilo Angry Birds – Parte 5: Destruindo o alvo

Possibilitamos ao jogador atirar com o canhão na estrutura de objetos rígidos.

Porém, todos os objetos rígidos são indestrutíveis e apenas sofrem movimentação, e não danos físicos.

Vamos implementar a destruição do vaso.

No último tutorial nós incluímos a funcionalidade que permitia ao jogador atirar balas de canhão na estrutura montada no Tiled. Incluímos um Sprite de seta que mostra a direção e a força que a bala será arremessada, bem como criamos, a cada bala atirada, um objeto rígido e um Sprite de bala de canhão. Submetemos um impulso inicial na bala de canhão para que ela seja realmente atirada e fizemos o Sprite seguir o objeto rígido para que a animação aconteça com naturalidade. Nesse tutorial, nós implementaremos a funcionalidade que dá um objetivo ao nosso jogo. Faremos com que o vaso se quebre caso ele seja atingido por algum objeto com força razoavelmente grande. Bora botar a mão na massa.

Primeiramente, abra o arquivo “HelloWorldScene.h” e modifique a seguinte linha de código:

b2Body* objetoRigidoVaso;

para ficar dessa forma:

static b2Body* objetoRigidoVaso;

Fizemos isso porque precisaremos ter acesso a essa variável a partir de um objeto externo, esse não pertencente à classe HelloWorld. Se deixarmos essa variável estática, poderemos acessá-la em qualquer parte do arquivo “HelloWorldScene.cpp”. Apesar de ser uma solução rápida e eficaz, precisamos também fazer uma modificação no arquivo “HelloWorldScene.cpp” para que ela surta efeito. Quando abrirmos esse arquivo, faremos a modificação necessária. Ainda no arquivo “HelloWorldScene.h”, adicione as seguintes linhas de código:

class DetectorContato : public b2ContactListener

{

    virtual void PostSolve(b2Contact *contact,const b2ContactImpulse *impulse);

};

logo abaixo dessa:

void menuCloseCallback(CCObject* pSender);

Acabamos de criar uma classe chamada “DetectorContato” que herda todos os métodos e variáveis da classe “b2ContactListener”. Precisamos fazer isso para que possamos ter acesso a cada colisão que ocorre entre os objetos rígidos existentes no mundo do Box2D. Enquanto há colisão entre dois objetos rígidos, o método “PostSolve” é chamado para que os objetos colidentes sofram a resposta da colisão. Ela é realizada automaticamente pelo próprio motor do Box2D, mas precisamos de algumas informações dele para podermos medir o impacto entre dois objetos em uma colisão. Precisamos disso, primeiramente, para detectar quando há uma colisão com o vaso e para verificar ela foi mais forte do que a rigidez do vaso.

Continuando, abra o arquivo “HelloWorldScene.cpp” e inclua as seguintes linhas de código:

b2Body* HelloWorld::objetoRigidoVaso;

bool vasoQuebrado = false;

logo abaixo dessa:

using namespace CocosDenshion;

Lembram da modificação que eu comentei que é necessária realizar no arquivo “HelloWorldScene.cpp” para que pudéssemos acessar a varável “objetoRigidoVaso” em qualquer lugar? A primeira linha adicionada especifica que a variável “objetoRigidoVaso”, pertencente à classe HelloWorld, poderá ser acessada em qualquer parte do arquivo “HelloWorldScene.cpp”. Foi criada também uma variável denominada “vasoQuebrado”. Essa variável existe porque, quando detectamos que o vaso recebeu um impulso muito forte durante uma colisão, ele quebra e precisa ser removido do mundo do Box2D e da tela do celular. Porém, não se pode destruir um objeto rígido dentro do método “PostSolve”, onde nós detectamos tal acontecimento. O motor do Box2D não dá esse suporte e precisamos destruir o vaso fora do método “PostSolve”. Essa variável nos ajuda a identificar o momento certo de remover o corpo rígido que representa o vaso no mundo do Box2D.

Nesse mesmo arquivo, adicione a seguinte linha de código:

HelloWorld::mundoFisico->SetContactListener(new HelloWorld::DetectorContato());

logo abaixo dessa:

HelloWorld::mundoFisico = new b2World(b2Vec2(0.0,-10.0));

Nesse momento, nós especificamos ao Box2D que a cada tratamento de resposta de colisão, os métodos da classe “DetectorContato” sejam chamados. Dessa forma, a cada resposta de colisão que acontece no mundo do Box2D, o método “PostSolve” será chamado. Note que ainda não implementamos esse método. Para isso, adicione no final do arquivo as seguintes linhas de código:

void HelloWorld::DetectorContato::PostSolve(b2Contact *contact,const b2ContactImpulse *impulse) {

    int i;

    float impulsoNormalTotal = 0;

    if(contact->GetFixtureA()->GetBody()==HelloWorld::objetoRigidoVaso||contact->GetFixtureB()->GetBody()==HelloWorld::objetoRigidoVaso) {

        for(i=0;i<impulse->count;i++)

            impulsoNormalTotal += impulse->normalImpulses[i];

        if(impulsoNormalTotal>20000)

            vasoQuebrado = true;

    }

}

O que fizemos no método “PostSolve”? Primeiramente verificamos se um dos dois objetos colidentes é o vaso. Note que nesse método fazemos referência à variável “objetoRigidoVaso”, essa pertencente à classe “HelloWorld”. Como o método “PostSolve” não pertence a classe “HelloWorld”, teríamos problemas se tentássemos acessar a variável “objetoRigidoVaso”. Precisávamos deixar essa variável estática para que pudéssemos acessá-la dentro do método “PostSolve”. Caso um dos objetos rígidos seja o vaso, verificamos o impulso total da colisão que um corpo rígido exerceu sobre o outro. Caso o impulso total exceder um valor máximo, significa que o vaso não aguentou o impacto e quebrou na colisão. Porém, o valor do impulso total é dividido entre os vários pontos de colisão que podem existir. Dessa forma, precisamos somar o impulso que acontece em cada ponto de colisão. Ao final, caso o impulso total seja superior a 20.000, então o vaso quebra e igualamos a variável “vasoQuebrado” ao valor verdadeiro.

Para finalizar, ainda não destruímos o corpo rígido que representa o vaso no mundo do Box2D e nem tiramos da tela do aparelho o Sprite que o representa. Então precisamos modificar o método “atualizaMundo” para que ele faça isso quando necessário. Substitua as seguintes linhas de código:

HelloWorld::vaso->setPosition(ccp(HelloWorld::objetoRigidoVaso->GetPosition().x*PTM_RATIO,HelloWorld::objetoRigidoVaso->GetPosition().y*PTM_RATIO));

HelloWorld::vaso->setRotation(-(HelloWorld::objetoRigidoVaso->GetAngle()*180)/M_PI);

por essas:

if(!vasoQuebrado) {

    HelloWorld::vaso->setPosition(ccp(HelloWorld::objetoRigidoVaso->GetPosition().x*PTM_RATIO,HelloWorld::objetoRigidoVaso->GetPosition().y*PTM_RATIO));

    HelloWorld::vaso->setRotation(-(HelloWorld::objetoRigidoVaso->GetAngle()*180)/M_PI);

}

else if(HelloWorld::vaso->isVisible()) {

    HelloWorld::mundoFisico->DestroyBody(HelloWorld::objetoRigidoVaso);

    HelloWorld::vaso->setVisible(false);

}

Notem que, enquanto o vaso não está quebrado, o jogo atualiza o Sprite conforme o objeto rígido que representa o vaso no mundo do Box2D. Quando o vaso é quebrado, a variável “vasoQuebrado” é modificada com o valor verdadeiro. A partir desse momento, o método “atualizaMundo” verifica se o Sprite ainda está sendo mostrado na tela. Caso esteja, então o objeto rígido que representa o vaso ainda não foi destruído. Então, o jogo exclui o objeto rígido do mundo do Box2D e deixa o Sprite do vaso invisível. Isso dá o aspecto de que o vaso quebrou e não faz mais parte do mundo mostrado na tela. O resultado do vaso quebrado é mostrado na Figura 1. Legal, não!? =]

Figura 1 - Vaso quebrado

Figura 1 – Vaso quebrado

Nesse tutorial, nós vimos como destruir o objeto alvo. Vimos que, primeiramente, precisamos detectar o momento em que ele foi quebrado para que o objeto seja removido posteriormente em outro método. Vimos também que a lógica para identificar quando o vaso quebra funciona por meio do cálculo do impulso que um objeto exerce sobre o outro em uma colisão. Caso o impulso seja maior do que o valor máximo que o vaso aguenta, ele quebra. Notem também que podemos aplicar isso não somente ao vaso, mas a qualquer objeto rígido existente na simulação. Eu foquei no vaso para não tornar esse conjunto de tutoriais muito longo e deixo essa ideia como um “exercício” para quem se interessou. Também podemos incluir animações para que o jogo fique um pouco mais dinâmico, como a animação de fumaça quando o canhão atira e a animação de explosão quando o vaso quebra. Vejam como podemos deixar o jogo mais legal a partir de agora. Incluiremos no próximo tutorial as HUDs que contam a quantidade de tiros que o jogador deu, e mostra o fim do jogo quando o vaso é quebrado.

Um grande abraço e até o próximo tutorial.

Santiago Viertel

Santiago Viertel

Formado em Bacharelado em Ciência da Computação (UDESC), mestre e doutorando em Análise de Algoritmos (UFPR). Foi programador da Céu Games por 8 anos. Possui a preferência por jogos de estratégia e de tiro em primeira pessoa. Jogando bastante DotA 2, Left 4 Dead 2 e Age of Empires II HD.

Send this to a friend