Tutorial: Criando um jogo ao estilo Angry Birds – Parte 3: Adicionando a simulação física

Criamos uma fase do nosso jogo ao estilo Angry Birds e vimos como ela ficou na tela do aparelho.

A fase, porém, está estática e não existe qualquer movimentação ou simulação física.

Vamos deixar esses objetos com vida. =]

Vimos no último tutorial como criarmos e editarmos uma fase do nosso jogo ao estilo Angry Birds (créditos a Rovio) no Tiled. Vimos que precisávamos separar os objetos em diferentes “classes”, para que pudéssemos abri-los como queríamos no Cocos2d-x. Se um objeto estava dentro do conjunto de objetos nomeado “Pedras”, então ele seria aberto no jogo como uma pedra e assim por diante. Criamos uma fase e a abrimos no nosso game.

Nesse tutorial nós incluiremos os efeitos físicos nesses objetos adicionados. Até abrimos a fase e vimos como ela ficaria, porém os Sprites apenas são imagens mostradas na tela e ainda não possuem efeitos físicos de pedra, de madeira e nem de aço (no caso das vigas). Vamos criar um mundo físico por meio da biblioteca Box2D e incluir nesse mundo objetos rígidos com a mesma geometria dos outros que criamos na fase. Além deles terem a geometria igual, eles também terão uma densidade (o valor de massa por volume), que define, digamos, a força que precisamos aplicar para mover um objeto. Você pode conhecer mais sobre a biblioteca e o motor físico Box2D fazendo a sequência de tutoriais começando por esse. Também fiz outro conjunto de tutoriais que gerou um jogo ao estilo aTilt (créditos a FrigdeCat Software) e que fazia bom uso do motor físico Box2D. Se você quiser saber sobre o desenvolvimento desse game, acesse o primeiro tutorial da sequência.

Vamos iniciar esse tutorial com a edição do código existente no arquivo “HelloWorldScene.h”. Se você não fez o tutorial anterior, o faça, por favor, porque não tem como dar continuidade sem você tê-lo feito. Adicione as seguintes linhas de código:

#include “Box2D/Box2D.h”

#define PTM_RATIO 40

logo abaixo dessa:

#include “cocos2d.h”

Nesse momento estamos abrindo a biblioteca Box2D para podermos utilizá-la no nosso game. Ainda no mesmo arquivo, adicione as seguintes linhas de código:

b2World* mundoFisico;

b2Body** objetosRigidos;

int qtdCorposRigidos;

b2Body* objetoRigidoVaso;

void atualizaMundo(float dt);

logo abaixo dessa:

cocos2d::CCSprite* vaso;

Criamos quatro variáveis: uma para armazenar uma referência ao objeto que simulará o mundo físico do Box2D; uma lista para armazenar todas as referências a todos os objetos rígidos da estrutura que protegerá o vaso; uma para armazenar a quantidade de objetos dessa estrutura; e uma para armazenar a referência ao objeto rígido que representa o vaso. Para quê isso tudo? Precisamos dessas referências para que possamos atualizar os Sprites com os valores de posição e rotação dos objetos rígidos conforme eles estão no mundo físico. Como o Box2D simula a física do negócio, basta atualizarmos os Sprites conforme os objetos rígidos estão no mundo físico.

Dando continuidade, salve e feche o arquivo “HelloWorldScene.h” e abra o arquivo “HelloWorldScene.cpp”. Adicione as seguintes linhas de código:

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

b2BodyDef definicaoParedes;

definicaoParedes.type = b2_staticBody;

b2Body* paredes = HelloWorld::mundoFisico->CreateBody(&definicaoParedes);

b2Vec2 vParedes[4];

vParedes[0].Set(0,size.height/PTM_RATIO);

vParedes[1].Set(size.width/PTM_RATIO,size.height/PTM_RATIO);

vParedes[2].Set(size.width/PTM_RATIO,((15.0/320.0)*fundo->boundingBox().size.height)/PTM_RATIO);

vParedes[3].Set(0,((15.0/320.0)*fundo->boundingBox().size.height)/PTM_RATIO);

b2ChainShape geometriaParedes;

geometriaParedes.CreateLoop(vParedes,4);

b2FixtureDef cascaParedes;

cascaParedes.shape = &geometriaParedes;

paredes->CreateFixture(&cascaParedes);

logo abaixo dessa:

addChild(fundo);

Primeiramente, criamos o mundo físico do Box2D com gravidade para baixo, logicamente. Logo após, adicionamos os limites da tela do aparelho para que esses objetos não saiam dela no meio da execução do jogo. Você até poderia deixar apenas o chão, se você quiser, mas aqui colocamos o chão, uma parede lateral em cada lado da tela e uma parede superior. Resumindo, fechamos o ambiente para que a simulação física ocorra somente dentro da tela do aparelho. Note que o chão está um pouco acima da parte de baixo da tela, assim como acontece no graficamente do Sprite do fundo. Agora, adicione as seguintes linhas de código:

HelloWorld::objetosRigidos = new b2Body*[

    tiledMap->objectGroupNamed(“Vigas”)->getObjects()->count() +

    tiledMap->objectGroupNamed(“Madeiras”)->getObjects()->count() +

    tiledMap->objectGroupNamed(“Pedras”)->getObjects()->count()];

HelloWorld::qtdCorposRigidos = 0;

logo abaixo dessa:

CCTMXTiledMap* tiledMap = CCTMXTiledMap::create(“fase1.tmx”);

Nessas linhas, nós criamos uma lista de objetos rígidos que armazenará a referência de cada objeto que modela a estrutura que protege o vaso. Note também que inicializamos a variável “qtdCorposRigidos” com o valor zero. Isso porque ela é incrementada a cada objeto criado e adicionado na lista. É possível perceber também que essa lista armazena somente os objetos rígidos que são “Pedras”, “Madeiras” e “Vigas”.

Agora, adicionaremos os corpos rígidos que representam cada objeto existente na estrutura que protege o vaso. Assim, precisamos criar esses corpos para cada Viga, Madeira e Pedra existente na fase que criamos no Tiled. Assim, editaremos o código existente dentro de cada estrutura de repetição “for” que vem logo adiante no código. Começamos criando os objetos rígidos para cada viga. Adicione as seguintes linhas de código:

b2BodyDef definicaoViga;

definicaoViga.type = b2_dynamicBody;

HelloWorld::objetosRigidos[HelloWorld::qtdCorposRigidos] = HelloWorld::mundoFisico->CreateBody(&definicaoViga);

if(largura<altura)

    HelloWorld::objetosRigidos[HelloWorld::qtdCorposRigidos]->SetTransform(b2Vec2(spriteObjeto->getPositionX()/PTM_RATIO,spriteObjeto->getPositionY()/PTM_RATIO),-M_PI/2);

else

    HelloWorld::objetosRigidos[HelloWorld::qtdCorposRigidos]->SetTransform(b2Vec2(spriteObjeto->getPositionX()/PTM_RATIO,spriteObjeto->getPositionY()/PTM_RATIO),0);

b2PolygonShape geometriaViga;

geometriaViga.SetAsBox(((spriteObjeto->getScaleX()*spriteObjeto->getContentSize().width)/PTM_RATIO)/2,((spriteObjeto->getScaleY()*spriteObjeto->getContentSize().height)/PTM_RATIO)/2);

b2FixtureDef cascaViga;

cascaViga.shape = &geometriaViga;

cascaViga.density = 7830;

HelloWorld::objetosRigidos[HelloWorld::qtdCorposRigidos]->CreateFixture(&cascaViga);

HelloWorld::qtdCorposRigidos++;

logo ACIMA dessas linhas:

    }

camadaObjetos = tiledMap->objectGroupNamed(“Madeiras”);

Note que criamos cada objeto rígido e incluímos uma referência a eles na lista que criamos antes, no arquivo “HelloWorldScene.h”. Assim, um objeto rígido na posição 1 dessa lista representa um Sprite que está na posição 1 da lista de Sprites que criamos no tutorial passado. Isso será útil quando atualizarmos cada Sprite conforme o objeto rígido que o representa. Outro ponto a atentar é que a viga pode estar deitada ou em pé. Nesse caso, precisamos rotacionar o objeto rígido que a representa no mundo físico, caso a altura dela seja maior do que a largura. Note que colocamos a densidade da viga como 7830 kg/m². Coloquei esse valor porque eu pesquisei a densidade do aço no Google e vi que era aproximadamente esse valor em metros cúbicos (m³).

Continuando, criaremos os objetos rígidos de cada bloco de madeira. Para isso, adicione as seguintes linhas de código:

b2BodyDef definicaoMadeira;

definicaoMadeira.position.Set(spriteObjeto->getPositionX()/PTM_RATIO,spriteObjeto->getPositionY()/PTM_RATIO);

definicaoMadeira.type = b2_dynamicBody;

HelloWorld::objetosRigidos[HelloWorld::qtdCorposRigidos] = HelloWorld::mundoFisico->CreateBody(&definicaoMadeira);

b2PolygonShape geometriaMadeira;

geometriaMadeira.SetAsBox(((spriteObjeto->getScaleX()*spriteObjeto->getContentSize().width)/PTM_RATIO)/2,((spriteObjeto->getScaleY()*spriteObjeto->getContentSize().height)/PTM_RATIO)/2);

b2FixtureDef cascaMadeira;

cascaMadeira.shape = &geometriaMadeira;

cascaMadeira.density = 785;

HelloWorld::objetosRigidos[HelloWorld::qtdCorposRigidos]->CreateFixture(&cascaMadeira);

HelloWorld::qtdCorposRigidos++;

logo ACIMA dessas:

    }

camadaObjetos = tiledMap->objectGroupNamed(“Pedras”);

O que é interessante atentar aqui é que não é necessário realizar a rotação do bloco, assim como fizemos na viga, e que a densidade da madeira é muito inferior à densidade do aço. Quando houver a simulação física, você poderá notar que uma viga é beeem mais difícil de movimentar do que um bloco de madeira. Agora crie os objetos rígidos de cada bloco de pedra, adicionando as seguintes linhas de código:

b2BodyDef definicaoPedra;

definicaoPedra.position.Set(spriteObjeto->getPositionX()/PTM_RATIO,spriteObjeto->getPositionY()/PTM_RATIO);

definicaoPedra.type = b2_dynamicBody;

HelloWorld::objetosRigidos[HelloWorld::qtdCorposRigidos] = HelloWorld::mundoFisico->CreateBody(&definicaoPedra);

b2PolygonShape geometriaPedra;

geometriaPedra.SetAsBox(((spriteObjeto->getScaleX()*spriteObjeto->getContentSize().width)/PTM_RATIO)/2,((spriteObjeto->getScaleY()*spriteObjeto->getContentSize().height)/PTM_RATIO)/2);

b2FixtureDef cascaPedra;

cascaPedra.shape = &geometriaPedra;

cascaPedra.density = 2750;

HelloWorld::objetosRigidos[HelloWorld::qtdCorposRigidos]->CreateFixture(&cascaPedra);

HelloWorld::qtdCorposRigidos++;

logo ACIMA dessas:

    }

camadaObjetos = tiledMap->objectGroupNamed(“Parametros”);

Note a densidade da pedra, que é igual a 2750 kg/m². Ela está entre a viga de aço e o bloco de madeira. Agora incluiremos o objeto rígido que representa o vaso com a densidade da argila, que é de aproximadamente 1700 kg/m². Adicione as seguintes linhas de código:

b2BodyDef definicaoVaso;

definicaoVaso.position.Set(HelloWorld::vaso->getPositionX()/PTM_RATIO,HelloWorld::vaso->getPositionY()/PTM_RATIO);

definicaoVaso.type = b2_dynamicBody;

HelloWorld::objetoRigidoVaso = HelloWorld::mundoFisico->CreateBody(&definicaoVaso);

b2PolygonShape geometriaVaso;

geometriaVaso.SetAsBox(((HelloWorld::vaso->getScaleX()*HelloWorld::vaso->getContentSize().width)/PTM_RATIO)/2,((HelloWorld::vaso->getScaleY()*HelloWorld::vaso->getContentSize().height)/PTM_RATIO)/2);

b2FixtureDef cascaVaso;

cascaVaso.shape = &geometriaVaso;

cascaVaso.density = 1700;

HelloWorld::objetoRigidoVaso->CreateFixture(&cascaVaso);

logo ABAIXO dessa:

addChild(HelloWorld::vaso);

Adicionaremos agora um objeto rígido em forma de retângulo no lugar no canhão. Esse objeto rígido será estático, ou seja, ele não se mexe como os outros objetos rígidos. Faremos isso porque o canhão também não se mexerá durante a simulação física, ficando sempre no mesmo lugar. Caso um objeto encoste nele, o objeto que encostou se move como se tivesse uma pedra muito pesada no lugar do canhão. Perceba também que o retângulo é posicionado mais para o lado de onde o canhão está apontando. Se ele estiver apontando para a direita, o retângulo é posicionado um pouco mais para a direita, por exemplo. Ao final do próximo código inserido, nós falamos para o Cocos2d-x executar o método “atualizaMundo” a cada quadro. Adicione o seguinte código:

b2BodyDef definicaoCanhao;

if(descricao->valueForKey(“orientacao”)->intValue()==0)

    definicaoCanhao.position.Set((suporteCanhao->getPositionX() – fundo->getScale()*33)/PTM_RATIO,(suporteCanhao->getPositionY() + fundo->getScale()*38)/PTM_RATIO);

else

    definicaoCanhao.position.Set((suporteCanhao->getPositionX() + fundo->getScale()*33)/PTM_RATIO,(suporteCanhao->getPositionY() + fundo->getScale()*38)/PTM_RATIO);

definicaoCanhao.type = b2_staticBody;

b2Body* canhao = HelloWorld::mundoFisico->CreateBody(&definicaoCanhao);

b2PolygonShape geometriaCanhao;

geometriaCanhao.SetAsBox((fundo->getScale()*33)/PTM_RATIO,(fundo->getScale()*38)/PTM_RATIO);

b2FixtureDef cascaCanhao;

cascaCanhao.shape = &geometriaCanhao;

canhao->CreateFixture(&cascaCanhao);

schedule(schedule_selector(HelloWorld::atualizaMundo));

logo ABAIXO dessa linha de código:

addChild(HelloWorld::suporteCanhao);

Agora só falta atualizar o mundo a cada quadro mostrado na tela do aparelho, para que a simulação física flua naturalmente. Atualizamos o mundo físico no método “atualizaMundo”. Esse método ainda não foi incluso no código e, além de atualizar o mundo físico, ele atualiza as posições e rotações dos Sprites conforme cada objeto rígido existente no mundo físico. Inclua as seguintes linhas de código no final do arquivo e já realizamos todas as modificações necessárias.

void HelloWorld::atualizaMundo(float dt) {

    int i;

    CCSprite* spr;

    HelloWorld::mundoFisico->Step(dt,3,2);

    for(i=0;i<HelloWorld::qtdCorposRigidos;i++) {

        spr = static_cast<CCSprite*>(HelloWorld::objetosRigidosSprites->objectAtIndex(i));

        spr->setPosition(ccp(HelloWorld::objetosRigidos[i]->GetPosition().x*PTM_RATIO,HelloWorld::objetosRigidos[i]->GetPosition().y*PTM_RATIO));

        spr->setRotation(-(HelloWorld::objetosRigidos[i]->GetAngle()*180)/M_PI);

    }

    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);

}

Note que no código adicionado nós atualizamos o Sprite, representado por cada objeto rígido que foi adicionado na fase. Essa atualização é feita na posição do Sprite e na rotação dele em cada momento da simulação. Precisamos também realizar uma conversão entre radianos graus. Isso porque o Box2D utiliza o padrão de rotação em radianos e o Cocos2d-x utiliza o padrão em graus. No final, atualizamos também o Sprite que representa o vaso e temos todo o nosso ambiente simulado fisicamente. Se você for executar o game agora, você não verá muita diferença para o último tutorial, mas nesse momento ele executa a simulação física. A Figura 1 mostra como estão os objetos rígidos no mundo físico e a Figura 2 mostra somente os Sprites, assim como ficará no nosso game. É possível notar que existe um objeto rígido verde (estático) no lugar do canhão, conforme implementamos no tutorial. Para você que está “boiando” e não faz ideia de como eu mostrei apenas os objetos rígidos e não os Sprites, olhe nos tutoriais que eu citei no começo. Lá fala como podemos mostrar apenas o mundo físico por meio do debug draw.

Figura 1 - Simulação física da fase criada

Figura 1 – Simulação física da fase criada

Figura 2 - Jogo executando

Figura 2 – Jogo executando

Vimos nesse tutorial como adicionar a funcionalidade de simulação física no nosso game. Para isso, precisamos criar um mundo físico e os objetos rígidos referentes a cada objeto que adicionamos na fase. Porém, vimos também que apenas criar o mundo não era suficiente. Tivemos que atualizar os Sprites com os valores de posição e rotação de cada objeto rígido, para que os Sprites se movimentem conforme os objetos no mundo físico. No próximo tutorial nós implementaremos o tiro da bala de canhão.

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