Tutorial: Criando um jogo ao estilo Flappy Bird – Parte 3: Geração dos postes

Fizemos o pato voar conforme o clique na tela.

Isso é legal, porém ainda não existe desafio no jogo, pois os postes ainda não vêm de encontro com o pato.

Vamos implementar a geração de postes e a movimentação do pato.

Vimos no último tutorial como fazer o pato voar. Para isso, nós criamos o mundo físico do Box2D com gravidade e adicionamos nesse mundo um objeto rígido circular (que representa o pato) e dois objetos rígidos (que representam o chão e o teto). Sobre o objeto rígido circular nós incluímos um Sprite do pato e adicionamos uma animação quando o jogador clica na tela. Nesse tutorial eu havia prometido a vocês a geração aleatória de tubos, porém, para deixar a sequência de tutoriais mais intuitiva, faremos hoje apenas a geração dos tubos com a passagem livre sempre na mesma altura do chão. Deixaremos a aleatoriedade das passagens para o próximo tutorial.

De início, tiraremos o código que se tornou desnecessário para a atual versão do game. No primeiro tutorial, nós inserimos alguns Sprites para vermos como ficaria o cenário do jogo. Esses Sprites eram aqueles que ilustravam os postes que o pato não poderia encostar. Os postes que ainda estão no jogo precisam ser tirados para que novos postes venham da direita para a esquerda, dando o aspecto de movimento à cena. Dessa forma, abra o arquivo “HelloWorldScene.cpp” e remova todo o bloco que se inicia nessa linha de código (inclusive):

CCSprite* pe1 = CCSprite::createWithSpriteFrameName(“PostePe.png”);

até essa (inclusive):

addChild(cabeca4);

Para que eu possa dar continuidade ao tutorial, eu preciso explicar algumas modificações que eu fiz no mundo físico e o motivo delas. Criamos o mundo físico com gravidade. Porém, eu a retirei para que os postes presos na parte superior da tela não caíssem e para que os postes presos no chão não sofressem atrito quando eles iniciassem o percurso. Tudo é apenas ponto de vista. Imagine você arremessando uma caneta no espaço, ela segue um caminho retilíneo com velocidade constante. É mais ou menos isso que faremos no nosso game. Arremessaremos os postes no mundo físico em um ambiente sem gravidade. Fazendo essa modificação, porém, o pato não cai mais como acontecia anteriormente. Dessa forma, precisaremos realizar algumas modificações no código que faz o mundo ficar sem gravidade, para que os postes andem como queremos e precisaremos adicionar a força da gravidade apenas no pato. Para deixarmos o mundo sem gravidade, modifique a seguinte linha de código:

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

de forma que ela fique assim:

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

Note que o valor da gravidade era 10 metros por segundo no eixo y e apontando para baixo e, agora, deixamos o mundo físico sem gravidade. Para incluirmos a gravidade apenas no pato, adicione as seguintes linhas de código:

int i;

CCSprite* spritesPoste;

b2Body* corposRigidosPoste;

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

HelloWorld::corpoPato->ApplyForceToCenter(b2Vec2(0.0,-10.0*HelloWorld::corpoPato->GetMass()));

logo abaixo dessa:

void HelloWorld::atualiza(float dt) {

Note que fizemos algo a mais além de aplicar a força da gravidade no pato. Adicionamos algumas linhas de código a mais que precisaremos posteriormente. Mas, a parte principal para o momento é a aplicação da força, que é igual à massa do objeto físico circular que representa o pato multiplicado pela aceleração da gravidade (10). Aplicação direta da física aprendida no ensino médio. =]

As próximas modificações exigem que editemos também o arquivo “HelloWorldScene.h”. Na verdade, seria interessante eu explicar a lógica que eu segui para o funcionamento da geração e exclusão dos postes do cenário. Criei uma lista de Sprites que armazena as referências dos Sprites que ilustram os postes presos na parte superior da tela e outra que armazena os Sprites dos postes presos na parte inferior. Essas duas listas mudam constantemente, de forma que novos Sprites sejam adicionados nelas quando um poste é criado e que os Sprites que não aparecem mais na tela sejam excluídos delas. Isso, para evitar que o jogo processe os postes que já passaram pela tela. Assim sendo, adicione as seguintes linhas de código no arquivo “HelloWorldScene.h”:

cocos2d::CCArray* postesSuperiores;

cocos2d::CCArray* postesInferiores;

void criaPostes();

logo abaixo dessa linha:

cocos2d::CCSprite* pato;

Note que criamos duas variáveis de lista, uma que armazena as referências dos Sprites dos postes presos no teto, nomeada “postesSuperiores”, e outra que armazena as referências dos Sprites dos postes presos no chão, nomeada “postesInferiores”. Também criamos o protótipo de um método, chamado “criaPostes”, que tem a função de criar dois novos postes no cenário. Fizemos todas as modificações necessárias no arquivo “HelloWorldScene.h”. Abra o arquivo “HelloWorldScene.cpp” e adicione as seguintes linhas de código:

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

HelloWorld::postesSuperiores->retain();

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

HelloWorld::postesInferiores->retain();

schedule(schedule_selector(HelloWorld::criaPostes),2.5);

logo abaixo dessas:

setTouchEnabled(true);

HelloWorld::corpoPato->ApplyLinearImpulse(b2Vec2(0.0,3.0),HelloWorld::corpoPato->GetWorldCenter());

Nas linhas de código recém-adicionadas, nós inicializamos as duas listas declaradas para que possamos incluir e excluir referências de Sprites. Também requisitamos ao Cocos2d-x que o método “criaPostes” seja chamado automaticamente no jogo a cada 2 segundos e meio de intervalo de tempo. Dando continuidade no raciocínio, vamos implementar este método. Adicione ao final do arquivo as seguintes linhas de código:

void HelloWorld::criaPostes() {

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

    b2BodyDef corpoPosteSuperiorDefinicao;

    corpoPosteSuperiorDefinicao.type = b2_dynamicBody;

    corpoPosteSuperiorDefinicao.position.Set((size.width+0.04*size.height)/PTM_RATIO,(0.8*size.height)/PTM_RATIO);

    corpoPosteSuperiorDefinicao.linearVelocity.Set(-2.0,0.0);

    b2Body* corpoPosteSuperior = HelloWorld::mundoFisico->CreateBody(&corpoPosteSuperiorDefinicao);

    b2PolygonShape geometriaPosteSuperior;

    geometriaPosteSuperior.SetAsBox((0.04*size.height)/PTM_RATIO,(0.2*size.height)/PTM_RATIO);

    b2FixtureDef cascaPosteSuperior;

    cascaPosteSuperior.shape = &geometriaPosteSuperior;

    cascaPosteSuperior.density = 1.0;

    corpoPosteSuperior->CreateFixture(&cascaPosteSuperior);

    CCSprite* posteCorpoSuperior = CCSprite::createWithSpriteFrameName(“PosteCorpo.png”);

    posteCorpoSuperior->setScaleX((0.08*size.height)/posteCorpoSuperior->boundingBox().size.height);

    posteCorpoSuperior->setScaleY((0.4*size.height)/posteCorpoSuperior->boundingBox().size.height);

    posteCorpoSuperior->setPosition(ccp(corpoPosteSuperior->GetWorldCenter().x*PTM_RATIO,corpoPosteSuperior->GetWorldCenter().y*PTM_RATIO));

    posteCorpoSuperior->setRotation(180);

    posteCorpoSuperior->setUserData(corpoPosteSuperior);

    addChild(posteCorpoSuperior);

    HelloWorld::postesSuperiores->addObject(posteCorpoSuperior);

    CCSprite* posteCabecaSuperior = CCSprite::createWithSpriteFrameName(“PosteCabeca.png”);

    posteCabecaSuperior->setScale((0.08*size.height)/posteCabecaSuperior->boundingBox().size.height);

    posteCabecaSuperior->setAnchorPoint(ccp(0.5,-(0.2-0.08)/0.08));

    posteCabecaSuperior->setPosition(ccp(corpoPosteSuperior->GetWorldCenter().x*PTM_RATIO,corpoPosteSuperior->GetWorldCenter().y*PTM_RATIO));

    posteCabecaSuperior->setRotation(180);

    posteCabecaSuperior->setUserData(corpoPosteSuperior);

    addChild(posteCabecaSuperior);

    HelloWorld::postesSuperiores->addObject(posteCabecaSuperior);

    b2BodyDef corpoPosteInferiorDefinicao;

    corpoPosteInferiorDefinicao.type = b2_dynamicBody;

    corpoPosteInferiorDefinicao.position.Set((size.width+0.04*size.height)/PTM_RATIO,(0.2*size.height)/PTM_RATIO);

    corpoPosteInferiorDefinicao.linearVelocity.Set(-2.0,0.0);

    b2Body* corpoPosteInferior = HelloWorld::mundoFisico->CreateBody(&corpoPosteInferiorDefinicao);

    b2PolygonShape geometriaPosteInferior;

    geometriaPosteInferior.SetAsBox((0.04*size.height)/PTM_RATIO,(0.2*size.height)/PTM_RATIO);

    b2FixtureDef cascaPosteInferior;

    cascaPosteInferior.shape = &geometriaPosteInferior;

    cascaPosteInferior.density = 1.0;

    corpoPosteInferior->CreateFixture(&cascaPosteInferior);

    CCSprite* posteCorpoInferior = CCSprite::createWithSpriteFrameName(“PosteCorpo.png”);

    posteCorpoInferior->setScaleX((0.08*size.height)/posteCorpoInferior->boundingBox().size.height);

    posteCorpoInferior->setScaleY((0.4*size.height)/posteCorpoInferior->boundingBox().size.height);

    posteCorpoInferior->setPosition(ccp(corpoPosteInferior->GetWorldCenter().x*PTM_RATIO,corpoPosteInferior->GetWorldCenter().y*PTM_RATIO));

    posteCorpoInferior->setUserData(corpoPosteInferior);

    addChild(posteCorpoInferior);

    HelloWorld::postesInferiores->addObject(posteCorpoInferior);

    CCSprite* posteCabecaInferior = CCSprite::createWithSpriteFrameName(“PosteCabeca.png”);

    posteCabecaInferior->setScale((0.08*size.height)/posteCabecaInferior->boundingBox().size.height);

    posteCabecaInferior->setAnchorPoint(ccp(0.5,-(0.2-0.08)/0.08));

    posteCabecaInferior->setPosition(ccp(corpoPosteInferior->GetWorldCenter().x*PTM_RATIO,corpoPosteInferior->GetWorldCenter().y*PTM_RATIO));

    posteCabecaInferior->setUserData(corpoPosteInferior);

    addChild(posteCabecaInferior);

    HelloWorld::postesInferiores->addObject(posteCabecaInferior);

    CCSprite* postePeInferior = CCSprite::createWithSpriteFrameName(“PostePe.png”);

    postePeInferior->setScale((0.04*size.height)/postePeInferior->boundingBox().size.height);

    postePeInferior->setAnchorPoint(ccp(0.5,0.2/0.04));

    postePeInferior->setPosition(ccp(corpoPosteInferior->GetWorldCenter().x*PTM_RATIO,corpoPosteInferior->GetWorldCenter().y*PTM_RATIO));

    postePeInferior->setUserData(corpoPosteInferior);

    addChild(postePeInferior);

    HelloWorld::postesInferiores->addObject(postePeInferior);

}

Adicionamos bastante código, mas a ideia por detrás é simples. Criamos dois objetos rígidos no mundo físico: um que representa o poste preso no teto e um que representa o poste preso no chão. Também criamos dois Sprites que ilustram o poste preso teto e três Sprites que ilustram o poste preso no chão. Os Sprites criados para o poste preso no teto são o corpo do poste e a lâmpada. Os Sprites criados para o poste preso no chão são o corpo, a lâmpada e o pé. A cada novo Sprite criado, ele é adicionado em uma das duas listas, dependendo de qual poste ele pertence. Cada Sprite criado nessa etapa referencia o corpo rígido no Box2D que ele segue, por meio do método “setUserData”. Posteriormente precisaremos fazer com que o Sprite ande junto com o corpo, sendo necessário o armazenamento dessa referência. Note que posicionamos os Sprites para que eles estejam exatamente sobre os corpos rígidos criados no mundo do Box2D. Também adicionamos uma velocidade aos corpos criados para que eles andem de encontro ao pato. Agora vamos finalizar a edição do código inserindo as seguintes linhas:

if(HelloWorld::postesSuperiores->count()>0) {

    spritesPoste = static_cast<CCSprite*>(HelloWorld::postesSuperiores->objectAtIndex(0));

    corposRigidosPoste = static_cast<b2Body*>(spritesPoste->getUserData());

    if(corposRigidosPoste->GetWorldCenter().x+(0.04*size.height)/PTM_RATIO<0.0) {

        removeChild(spritesPoste);

        HelloWorld::postesSuperiores->removeObjectAtIndex(0);

        spritesPoste = static_cast<CCSprite*>(HelloWorld::postesSuperiores->objectAtIndex(0));

        removeChild(spritesPoste);

        HelloWorld::postesSuperiores->removeObjectAtIndex(0);

        HelloWorld::mundoFisico->DestroyBody(corposRigidosPoste);

        spritesPoste = static_cast<CCSprite*>(HelloWorld::postesInferiores->objectAtIndex(0));

        corposRigidosPoste = static_cast<b2Body*>(spritesPoste->getUserData());

        removeChild(spritesPoste);

        HelloWorld::postesInferiores->removeObjectAtIndex(0);

        spritesPoste = static_cast<CCSprite*>(HelloWorld::postesInferiores->objectAtIndex(0));

        removeChild(spritesPoste);

        HelloWorld::postesInferiores->removeObjectAtIndex(0);

        spritesPoste = static_cast<CCSprite*>(HelloWorld::postesInferiores->objectAtIndex(0));

        removeChild(spritesPoste);

        HelloWorld::postesInferiores->removeObjectAtIndex(0);

        HelloWorld::mundoFisico->DestroyBody(corposRigidosPoste);

    }

}

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

    spritesPoste = static_cast<CCSprite*>(HelloWorld::postesSuperiores->objectAtIndex(i));

    corposRigidosPoste = static_cast<b2Body*>(spritesPoste->getUserData());

    spritesPoste->setPosition(ccp(corposRigidosPoste->GetWorldCenter().x*PTM_RATIO,corposRigidosPoste->GetWorldCenter().y*PTM_RATIO));

}

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

    spritesPoste = static_cast<CCSprite*>(HelloWorld::postesInferiores->objectAtIndex(i));

    corposRigidosPoste = static_cast<b2Body*>(spritesPoste->getUserData());

    spritesPoste->setPosition(ccp(corposRigidosPoste->GetWorldCenter().x*PTM_RATIO,corposRigidosPoste->GetWorldCenter().y*PTM_RATIO));

}

logo abaixo dessa:

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

Como criamos corpos rígidos que se movimentam em direção ao pato, precisamos fazer com que os Sprites que ilustram os postes andem junto com eles. Também precisamos saber quando dois postes saem da tela para que possamos excluir os corpos rígidos que os representam no mundo físico e os Sprites que ilustram os postes. Primeiramente, a cada quadro, nós verificamos se o corpo rígido localizado mais à esquerda (o primeiro posicionado na lista) está fora da tela do aparelho. Caso ele esteja, então os postes presos no teto e no chão localizados mais à esquerda já saíram da tela. Caso isso aconteça, os Sprites que os ilustram são removidos das listas, da tela e da memória do aparelho. Também são removidos os dois corpos rígidos que representam os postes no mundo físico. Para finalizar, os Sprites que ainda estão nas listas e, que ainda estão sendo mostrados na tela, são posicionados sobre os corpos rígidos que representam os postes mostrados na tela. Se você compilar o código e rodar o game, aparecerá algo como o que é mostrado na Figura 1. Simples não? =]

Figura 1 - Jogo executando

Figura 1 – Jogo executando

Vimos nesse tutorial como gerar postes a cada 2 segundos e meio no cenário do jogo. Para isso criamos dois objetos rígidos, um para o poste preso no teto e um para o poste preso no chão. Também criamos dois Sprites que compõem o poste preso no teto e três Sprites que compõem o poste preso no chão. Para que o jogo não ficasse sobrecarregado com processamento, nós incluímos esses Sprites em uma lista e os removemos da memória quando eles saem da tela. A mesma coisa fizemos com os corpos rígidos no mundo físico. No próximo tutorial nós faremos com que os postes sejam criados de forma que a passagem entre eles esteja em uma altura que varia aleatoriamente durante o jogo.

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