Tutorial: Desenvolvendo um shoot em up game: Parte 5 – Explosões

Já fizemos os inimigos se movimentarem por conta própria e também tanto o jogador como os adversários atirarem.

Hoje implementaremos as explosões que ocorrem quando um tiro bate em uma nave inimiga ou na do jogador.

Falta pouco para termos em mãos um jogo shoot em up.

 

Todos os tutoriais da sequência
Parte 1Parte 2Parte 3Parte 4 – Parte 5 – Parte 6

GatoAtirandoVimos no último tutorial como incluir as funcionalidades de tiro do jogador e dos inimigos. Para que o jogador atirasse, nós primeiro identificamos quando ele pressiona e dedo sobre o botão de fogo. Quando isso acontece, nós fazemos um método da classe “HelloWorld” executar repetidas vezes, para que desse um aspecto de metralhadora. Para que os inimigos atirassem, nós criamos outro método, que é executado de tempos em tempos e escolhe um inimigo para disparar contra o jogador.

Nesse tutorial, nós implementaremos tudo o que acontece quando um tiro alcança um inimigo. De forma geral, o processamento de um tiro que alcança um inimigo é parecido com o de um tiro que alcança a nave do jogador. Deixaremos invisível a nave que recebeu o tiro, iniciaremos uma animação de explosão no lugar onde ela se encontra e faremos com que ela reapareça na tela.

 

Guardando as referências dos tiros

CapsulasPara que possamos identificar se um tiro alcança uma nave inimiga, por exemplo, precisamos ter as referências dos sprites do tiro e da nave inimiga. Da última, nós já temos as referências na classe “HelloWorld”, só está faltando armazenarmos em uma lista as referências dos sprites dos tiros disparados pelo jogador. Além disso, para que possamos identificar se um tiro inimigo acertou a nave do jogador, nós também precisaremos armazenar em outra lista as referências dos sprites dos tiros das naves inimigas.

Iniciaremos o tutorial de hoje incluindo os códigos que criam dois vetores, que armazenarão as referências dos sprites dos tiros. Separaremos os tiros disparados em dois vetores: um que armazena os tiros disparados pelo jogador e outro que armazena os tiros disparados pelos inimigos. Abra o arquivo “HelloWorldScene.h” e adicione as seguintes linhas de código:

cocos2d::CCArray* tirosJogador;

cocos2d::CCArray* tirosInimigos;

logo abaixo dessa:

int atirando;

EinsteinAgora precisaremos utilizar os vetores de forma inteligente. Primeiro inicializaremos os vetores para que possamos incluir as referências dos sprites quando for necessário. Para isso, abra o arquivo “HelloWorldScene.cpp” e adicione as seguintes linhas de código:

HelloWorld::tirosJogador = CCArray::create();

HelloWorld::tirosJogador->retain();

HelloWorld::tirosInimigos = CCArray::create();

HelloWorld::tirosInimigos->retain();

logo baixo dessa:

addChild(HelloWorld::botaoTiro,3);

Com os vetores inicializados, poderemos adicionar e remover as referências dos sprites dos tiros. Vale pensar em qual momento precisamos fazer isso. A melhor hora para incluir a referência de um sprite no vetor é quando o sprite do tiro é adicionado na cena. Para incluirmos na lista de sprites a referência de um sprite de tiros disparados pela nave do jogador, adicione a seguinte linha de código no final do método “criaTiroJogador”.

HelloWorld::tirosJogador->addObject(tiro);

Da mesma forma, para adicionarmos na lista de sprites a referência de um sprite que representa o tiro de um inimigo, adicione a seguinte linha de código no final do método “atiraInimigo”.

HelloWorld::tirosInimigos->addObject(tiro);

Agora precisamos remover os sprites da lista no momento em que eles não fazem mais parte da cena. Para lembrar, os sprites dos tiros são retirados do jogo quando o método “destroiTiro” é chamado. Então, para retirarmos das listas de tiros a referência de um tiro qualquer que saiu da cena, adicione as seguintes linhas de código no final do método “destroiTiro”.

HelloWorld::tirosJogador->removeObject(no);

HelloWorld::tirosInimigos->removeObject(no);

Agora poderemos identificar quando um tiro disparado por um oponente acerta o jogador e quando um tiro disparado pelo jogador acerta um oponente.

 

Contato entre os tiros e naves

ImpactoMissilFizemos no tutorial passado com que os tiros animassem automaticamente por meio de um sprite e fizemos com que eles também se autodestruíssem quando chegassem ao final da tela. Isso foi suficiente para nós … até o momento.

Precisaremos executar, a cada quadro, um método que verifica se há colisão entre os sprites de tiro e os sprites dos inimigos. Além disso, esse método deverá verificar se há colisão entre os sprites de tiro e da nave do jogador. Chamaremos esse método de “verificaColisaoTiros”. Precisaremos, também, de outros dois métodos: um que destrói os sprites de explosão quando a animação de explosão termina, que chamaremos de “destroiExplosao”; e outro que faz a nave do jogador reaparecer na tela após o término da animação de explosão, que chamaremos de “aparecerJogador”. Vamos declarar esses métodos na classe “HelloWorld”. Abra o arquivo “HelloWorldScene.h” e adicione as seguintes linhas de código:

void verificaColisaoTiros();

void destroiExplosao(cocos2d::CCNode* no);

void aparecerJogador();

logo abaixo dessa:

cocos2d::CCArray* tirosInimigos;

Agora implementaremos esses três métodos. Adicione as seguintes linhas de código no final do arquivo “HelloWorldScene.cpp”.

void HelloWorld::verificaColisaoTiros() {

    int i,j;

    CCSprite* tiro;

    CCSize size = CCDirector::sharedDirector()->getWinSize();

    for(i=0;i<HelloWorld::tirosJogador->count();i++) {

        tiro = static_cast<CCSprite*>(HelloWorld::tirosJogador->objectAtIndex(i));

        for(j=0;j<6;j++)

            if(HelloWorld::inimigos[j]->isVisible()&&

              tiro->boundingBox().intersectsRect(HelloWorld::inimigos[j]->boundingBox())) {

                HelloWorld::tirosJogador->removeObject(tiro);

                removeChild(tiro);

                HelloWorld::inimigos[j]->setVisible(false);

                HelloWorld::sombraInimigos[j]->setVisible(false);

                CCSprite* explosao = CCSprite::createWithSpriteFrameName(“Explosao1.png”);

                explosao->setScale((0.07*size.width)/explosao->boundingBox().size.width);

                explosao->setPosition(HelloWorld::inimigos[j]->getPosition());

                addChild(explosao,2);

                CCArray* frames = CCArray::create();

                frames->addObject(CCSpriteFrameCache::sharedSpriteFrameCache()->spriteFrameByName(“Explosao1.png”));

                frames->addObject(CCSpriteFrameCache::sharedSpriteFrameCache()->spriteFrameByName(“Explosao2.png”));

                frames->addObject(CCSpriteFrameCache::sharedSpriteFrameCache()->spriteFrameByName(“Explosao3.png”));

                CCAnimate* anim = CCAnimate::create(CCAnimation::createWithSpriteFrames(frames,0.1));

                CCCallFuncN* destruir = CCCallFuncN::create(this,callfuncN_selector(HelloWorld::destroiExplosao));

                explosao->runAction(CCSequence::create(anim,destruir,NULL));

                break;

            }

    }

    for(i=0;i<HelloWorld::tirosInimigos->count();i++) {

        tiro = static_cast<CCSprite*>(HelloWorld::tirosInimigos->objectAtIndex(i));

        if(tiro->boundingBox().intersectsRect(HelloWorld::jogador->boundingBox())) {

            HelloWorld::tirosInimigos->removeObject(tiro);

            removeChild(tiro);

            HelloWorld::jogador->setVisible(false);

            HelloWorld::sombraJogador->setVisible(false);

            CCSprite* explosao = CCSprite::createWithSpriteFrameName(“Explosao1.png”);

            explosao->setScale((0.07*size.width)/explosao->boundingBox().size.width);

            explosao->setPosition(HelloWorld::jogador->getPosition());

            addChild(explosao,2);

            CCArray* frames = CCArray::create();

            frames->addObject(CCSpriteFrameCache::sharedSpriteFrameCache()->spriteFrameByName(“Explosao1.png”));

            frames->addObject(CCSpriteFrameCache::sharedSpriteFrameCache()->spriteFrameByName(“Explosao2.png”));

            frames->addObject(CCSpriteFrameCache::sharedSpriteFrameCache()->spriteFrameByName(“Explosao3.png”));

            CCAnimate* anim = CCAnimate::create(CCAnimation::createWithSpriteFrames(frames,0.1));

            CCCallFuncN* destruir = CCCallFuncN::create(this,callfuncN_selector(HelloWorld::destroiExplosao));

            CCCallFunc* aparecer = CCCallFunc::create(this,callfunc_selector(HelloWorld::aparecerJogador));

            explosao->runAction(CCSequence::create(anim,destruir,aparecer,NULL));

        }

    }

}

void HelloWorld::destroiExplosao(CCNode* no) {

    removeChild(no);

}

void HelloWorld::aparecerJogador() {

    HelloWorld::jogador->setVisible(true);

    HelloWorld::sombraJogador->setVisible(true);

}

BalasColisaoNotem que no método “verificaColisaoTiros” nós fazemos a checagem de colisão para cada sprite de tiro existente nas listas que criamos no início do tutorial. Essas checagens são realizadas em duas etapas: uma para os tiros disparados pelo jogador e outra para os disparados pelos inimigos. Primeiro são verificadas as colisões entre cada sprite de tiro disparado pelo jogador e todos os sprites dos inimigos. Logo após, são verificadas as colisões entre cada sprite de tiro disparado pelos inimigos e o sprite da nave do jogador.

Caso haja colisão entre um sprite de tiro disparado pelo jogador e um de inimigo, então o sprite do tiro é retirado da tela e da lista de tiros. Também fazemos com que o sprite do inimigo fique invisível e criamos um sprite que faz uma animação de explosão na tela. Note que, após a animação de explosão, o método “destroiExplosao” é chamado para retirar da cena o sprite que fez o procedimento. Vale lembrar que essa checagem de colisão é realizada apenas com sprites que estão visíveis no momento, para evitar checagem de colisão com helicópteros que já foram destruídos.

Caso seja detectada uma colisão entre um sprite de tiro disparado pelo inimigo e o da nave do jogador, nós retiramos o sprite de tiro da cena e da lista de tiros. Criamos um sprite que anima uma explosão e chamamos os métodos “destroiExplosao”, para retirar o sprite de explosão da tela após o término da animação, e “aparecerJogador” para fazer com que a nave do jogador volte e aparecer na tela.

Para que funcione o sistema de detecção e resposta de colisão, precisamos executar o método “verificaColisaoTiros” a cada quadro processado. Para fazermos isso, adicione a seguinte linha de código:

schedule(schedule_selector(HelloWorld::verificaColisaoTiros));

logo abaixo dessa:

setTouchEnabled(true);

Pronto. Agora precisaremos arrumar alguns bugs que aparecem naturalmente com a implementação dessas funcionalidades.

 

Somente naves vivas atiram

MortoVivoCom a implementação das funcionalidades propostas no tutorial, aparecem alguns problemas que precisam ser resolvidos para o jogo ficar coerente. Se você olhar a implementação do método “atiraInimigo”, perceberá que não é feita nenhuma checagem se o inimigo escolhido aleatoriamente para atirar já foi abatido pelo jogador. Se o jogo for executado agora, alguns inimigos atirarão mesmo não existindo mais.

Para solucionar esse problema, basta fazermos consecutivos sorteios de inimigos, até selecionar algum que ainda não foi explodido por um tiro do jogador. Para isso, no método “atiraInimigo”, adicione a seguinte linha de código:

do {

logo abaixo dessa:

CCSize size = CCDirector::sharedDirector()->getWinSize();

e adicione a seguinte linha de código:

} while(!HelloWorld::inimigos[i]->isVisible());

logo ACIMA dessa:

CCSprite* tiro = CCSprite::createWithSpriteFrameName(“InimigoTiro.png”);

Programadores mais experientes notarão que ainda não solucionamos o problema por completo. Quando o jogador explode TODOS os helicópteros inimigos e o método “atiraInimigo” é chamado, acontecerá um loop infinito exatamente nesse lugar onde modificamos o código. Como são realizados consecutivos sorteios até achar um inimigo que ainda não foi abatido, o programa fica parado nessas instruções, tentando encontrar um inimigo que está vivo. Como todos os inimigos estão mortos, então o programa não para de sortear inimigos. Para evitar que ocorra esse loop infinito, basta adicionar, no mesmo método, as seguintes linhas de código:

if(HelloWorld::inimigos[0]->isVisible()||HelloWorld::inimigos[1]->isVisible()||

  HelloWorld::inimigos[2]->isVisible()||HelloWorld::inimigos[3]->isVisible()||

  HelloWorld::inimigos[4]->isVisible()||HelloWorld::inimigos[5]->isVisible()) {

logo abaixo dessa:

CCSize size = CCDirector::sharedDirector()->getWinSize();

e a seguinte linha de código no final do método.

}

ExplosaoNaveCom isso, os sorteios somente ocorrerão se houver pelo menos um inimigo vivo.

Agora, solucionaremos outro problema que acontece no momento que a nave do jogador explode. Se ela iniciar uma explosão, ele ainda poderá atirar contra seus inimigos. Isso porque não é feita nenhuma checagem se o jogador está vivo antes da nave disparar um tiro. Para solucionar esse problema, no método “criaTiroJogador”, adicione a seguinte linha de código:

if(HelloWorld::jogador->isVisible()) {

logo abaixo dessa:

CCSize size = CCDirector::sharedDirector()->getWinSize();

e adicione a seguinte linha de código no final do mesmo método.

}

Nossa … esse tutorial foi longo. Agora sim, você pode compilar o código e executar o jogo. Você poderá ver o resultado dos últimos tutoriais no vídeo a seguir. Note que nenhum helicóptero inimigo que já foi explodido atira e que não acontece nenhum problema quando todos eles são destruídos. Além disso, perceba também que a nave do jogador não dispara tiro enquanto acontece a animação de explosão dela.

 

Vimos nesse tutorial como incluir as funcionalidades de explosão dos inimigos e da nave do jogador. Para isso, precisamos criar duas listas que armazenam as referências dos sprites de tiros. A cada quadro, nós verificamos se algum tiro disparado pelo jogador acertou algum inimigo e se algum tiro disparado por uma nave inimiga acertou o jogador. Por último, nós corrigimos alguns bugs decorrentes da implementação dessas funcionalidades.

No próximo tutorial nós adicionaremos os elementos de HUD e finalizaremos mais um game para o nosso portfólio.

Um grande abraço e até mais. []

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