Tutorial: Desenvolvendo um Jogo de Sinuca em Cocos2d-x: Parte 3 – Física de movimentação das bolas

até o momento, já incluímos a simulação física no nosso jogo de sinuca.

Não nos preocupamos ainda com o fato das bolas pararem de rolar com o tempo.

hoje, implementaremos a ação das forças de atrito das bolas com a superfície da mesa.

Parte 1Parte 2 – Parte 3 – Parte 4Parte 5Parte 6

Criamos no último tutorial o ambiente de simulação física do nosso jogo. Retiramos a força da gravidade e criamos os objetos rígidos das bolas e da mesa de bilhar. Por último, nós acionamos o debug draw, para termos mais informações sobre os objetos rígidos pertencentes à simulação.

Implementaremos no tutorial de hoje o sistema de movimentação das bolas de sinuca. Porém, isso não é tão simples e direto de se implementar. Veremos que é preciso aplicar forças de atrito em determinados momentos e necessário retirar tais forças em outros momentos.

Tiramos a força de gravidade da simulação no último tutorial. Isso porque essa força está, na verdade, puxando as bolas para dentro da tela. Na programação de jogos, esse tipo de câmera confunde muitos novatos. Um pensamento natural nesse momento é: o motor físico do Cocos2d-x realiza a movimentação dos corpos aplicando também forças de atrito. Isso significa que eu poderei utilizar esse motor de forma que as bolas parem de rolar com o tempo, devido à aplicação de forças de atrito com a superfície mesa. Esse pensamento não está errado, porém o motor físico do Cocos2d-x realiza cálculos de atrito somente durante a interação entre os corpos rígidos da simulação física. Ou seja, o atrito só acontece quando as bolas colidem entre si ou com as paredes da mesa, sendo desconsiderado o atrito com a superfície da mesa.

ArrastandoDiante disso, apresento-lhes a pergunta que não quer calar: como implementar o atrito entre as bolas e a superfície da mesa para que as bolas parem de rolar com o tempo? Isso, meus caros amigos leitores, é necessário implementar por conta. Parece complicado, mas vocês verão que não é tanto assim.

Para concluirmos o tutorial de hoje, nós criaremos um detector de colisão de corpos rígidos e aplicaremos forças de atrito nos corpos rígidos colidentes após cada colisão. Mãos na massa.

 

Implementando a detecção de colisão

Para podermos adicionar o atrito das bolas com a mesa, é preciso aplicar uma força de atrito nas bolas sempre que elas mudarem a direção do movimento. Isso porque, na física, a força de atrito é aplicada no sentido oposto ao movimento. Assim, sempre que uma bola colide e muda a direção do movimento, a direção da força de atrito precisa ser atualizada.

Sabendo o motivo de implementar um detector de colisão, iniciaremos essa primeira parte selecionando os corpos rígidos que precisam ter suas colisões devidamente detectadas. Precisamos identificar os corpos rígidos porque o Cocos2d-x detecta colisões somente entre os corpos rígidos previamente selecionados pelo programador. Para selecionarmos os corpos rígidos das bolas e das paredes da mesa para a detecção de colisão, abra o arquivo “HelloWorldScene.cpp” e adicione a seguinte linha de código:

HelloWorld::bolaBrancaCorpo->setContactTestBitmask(1);

logo abaixo dessa:

HelloWorld::bolaBranca->getContentSize().width/2,PhysicsMaterial(1.0,1.0,0.0));

adicione a seguinte linha de código:

HelloWorld::bolaVermelhaCorpo->setContactTestBitmask(1);

logo abaixo dessa:

HelloWorld::bolaVermelha->getContentSize().width/2,PhysicsMaterial(1.0,1.0,0.0));

adicione a seguinte linha de código:

HelloWorld::bolaAmarelaCorpo->setContactTestBitmask(1);

logo abaixo dessa:

HelloWorld::bolaAmarela->getContentSize().width/2,PhysicsMaterial(1.0,1.0,0.0));

e adicione a seguinte linha de código:

HelloWorld::mesaCorpo->setContactTestBitmask(1);

logo abaixo dessa:

HelloWorld::mesaCorpo->setDynamic(false);

ColisaoObviamente, apenas selecionar os corpos rígidos que devem detectar colisões não é o suficiente. Também é preciso fazer com que a classe “HelloWorld” implemente o método “onContactPostSolve”. Esse processo é feito após a realização dos cálculos de colisão e atualização das movimentações dos dois corpos rígidos colidentes, pois somente após as movimentações dos objetos rígidos terem sido atualizadas é que se pode calcular a direção da força de atrito a ser aplicada em cada um dos corpos rígidos. Para adicionarmos esse método na classe “HelloWorld”, abra o arquivo “HelloWorldScene.h” e adicione a seguinte linha de código:

void onContactPostSolve(cocos2d::PhysicsContact& contact,const cocos2d::PhysicsContactPostSolve& solve);

logo abaixo dessa:

cocos2d::PhysicsBody* mesaCorpo;

Também precisamos que esse método seja implementado para que não haja erro de compilação. Abra o arquivo “HelloWorldScene.cpp” e adicione as seguintes linhas de código no final do arquivo.

void HelloWorld::onContactPostSolve(cocos2d::PhysicsContact& contact,const cocos2d::PhysicsContactPostSolve& solve) {

}

Agora podemos criar o objeto detector e fazer com que o método “onContactPostSolve” seja chamado sempre quando há colisões entre os corpos rígidos selecionados anteriormente. Para finalizar a programação da funcionalidade de detecção de colisão, adicione as seguintes linhas de código:

EventListenerPhysicsContact* contato = EventListenerPhysicsContact::create();

contato->onContactPostSolve = CC_CALLBACK_2(HelloWorld::onContactPostSolve,this);

getEventDispatcher()->addEventListenerWithSceneGraphPriority(contato,this);

logo abaixo dessa:

HelloWorld::mesa->addComponent(HelloWorld::mesaCorpo);

Pronto. Agora a funcionalidade de detecção de colisão está devidamente implementada. Porém, apesar do método “onContactPostSolve” estar sendo chamado sempre quando há uma colisão entre dois corpos rígidos, nada está sendo feito em relação às colisões detectadas.

Então, próximo passo: aplicar a força de atrito nos corpos rígidos das bolas de sinuca após a detecção de colisões.

 

Aplicando as forças de atrito

ForcaAntes de sairmos calculando forças de atrito e aplicando-as, precisamos entender como elas funcionam e em quais momentos elas devem ser aplicadas. No motor físico do Cocos2d-x, quando realizamos a chamada do método responsável por aplicar uma força em um corpo rígido, essa força é aplicada continuamente no corpo. Isso significa que, a partir do momento que o método é chamado, o corpo rígido passa a sofrer a ação da força a todo instante.

Quando aplicamos uma força de atrito após uma colisão, por exemplo, a força de atrito exercerá ação no corpo rígido durante toda a simulação física. Esse sistema parece atender ao que necessitamos, porém existe um problema a ser solucionado. Quando o corpo rígido entra no estado de repouso, a força de atrito aplicada nele deve cessar. Caso não seja cessada, o corpo rígido terá uma aceleração no sentido oposto depois que ele parar.

Tá Santy, mas o que você quer dizer com isso tudo? Imagine as bolas parando de rolar com tempo e, após elas pararem, elas começarem a voltar pelo mesmo caminho percorrido anteriormente. Isso é o que acontece se não cessarmos a força de atrito aplicada anteriormente no corpo. Mas como podemos resolver esse problema? A melhor forma é: calcular a força de atrito, aplicá-la, calcular o tempo correto que essa força deve ter aplicada continuamente e cessá-la após esse tempo terminar. Pensando dessa forma, criaremos três métodos na classe “HelloWorld” responsáveis por cessar todas as forças atuantes nos corpos rígidos das bolas. Abra o arquivo “HelloWorldScene.h”, e declare os protótipos desses três métodos adicionando as seguintes linhas de código:

void paraBolaBranca(float dt);

void paraBolaVermelha(float dt);

void paraBolaAmarela(float dt);

logo abaixo dessa:

void onContactPostSolve(cocos2d::PhysicsContact& contact,const cocos2d::PhysicsContactPostSolve& solve);

Agora vamos implementá-los. Adicione as seguintes linhas de código no final do arquivo “HelloWorldScene.cpp”.

void HelloWorld::paraBolaBranca(float dt) {

    HelloWorld::bolaBrancaCorpo->resetForces();

    HelloWorld::bolaBrancaCorpo->setVelocity(Vec2::ZERO);

    unschedule(schedule_selector(HelloWorld::paraBolaBranca));

}

void HelloWorld::paraBolaVermelha(float dt) {

    HelloWorld::bolaVermelhaCorpo->resetForces();

    HelloWorld::bolaVermelhaCorpo->setVelocity(Vec2::ZERO);

    unschedule(schedule_selector(HelloWorld::paraBolaVermelha));

}

void HelloWorld::paraBolaAmarela(float dt) {

    HelloWorld::bolaAmarelaCorpo->resetForces();

    HelloWorld::bolaAmarelaCorpo->setVelocity(Vec2::ZERO);

    unschedule(schedule_selector(HelloWorld::paraBolaAmarela));

}

CabecaDuraO que fizemos em cada um dos métodos? Retiramos todas as forças aplicadas nos corpos rígidos, paramos os corpos rígidos das bolas e desprogramamos a próxima execução do método, visto que eles executam somente uma vez quando a bola entra em repouso.

Para finalizar o tutorial de hoje, aplicaremos a força de atrito nos corpos rígidos das bolas após detecções de colisão que envolvam elas. No arquivo “HelloWorldScene.cpp”, adicione as seguintes linhas de código:

static float g = 9.8;

static float mi = 3.0;

PhysicsBody* bodies[2] = {contact.getShapeA()->getBody(),contact.getShapeB()->getBody()};

Vec2 dir,fa;

float t;

for(int i=0;i<2;i++)

    if(bodies[i]!=HelloWorld::mesaCorpo) {

        bodies[i]->resetForces();

        dir = bodies[i]->getVelocity();

        dir.normalize();

        dir.negate();

        fa = dir.operator*(bodies[i]->getMass()*g*mi);

        bodies[i]->applyForce(fa);

        t = (bodies[i]->getVelocity().length()*bodies[i]->getMass())/fa.length();

        if(bodies[i]==HelloWorld::bolaBrancaCorpo) {

            unschedule(schedule_selector(HelloWorld::paraBolaBranca));

            schedule(schedule_selector(HelloWorld::paraBolaBranca),t);

        } else if(bodies[i]==HelloWorld::bolaVermelhaCorpo) {

            unschedule(schedule_selector(HelloWorld::paraBolaVermelha));

            schedule(schedule_selector(HelloWorld::paraBolaVermelha),t);

        } else if(bodies[i]==HelloWorld::bolaAmarelaCorpo) {

            unschedule(schedule_selector(HelloWorld::paraBolaAmarela));

            schedule(schedule_selector(HelloWorld::paraBolaAmarela),t);

        }

}

logo abaixo dessa:

void HelloWorld::onContactPostSolve(cocos2d::PhysicsContact& contact,const cocos2d::PhysicsContactPostSolve& solve) {

Nessas linhas de código, nós aplicamos a força de atrito nos corpos rígidos das bolas após a detecção de colisão. Note que nós calculamos a força de atrito com direção oposta à velocidade do corpo rígido da bola. Também calculamos o tempo necessário para a bola entrar em repouso e, logo após, programamos a execução do respectivo método para cessar as forças atuantes e parar a movimentação da bola. Notem também que verificamos se o corpo rígido não é o das paredes da mesa. Isso porque existem colisões entre bolas e as paredes da mesa. Como a mesa não sofre nenhuma ação resultante de forças, nada é realizado quando o corpo rígido for o das paredes da mesa.

Agora sim. A funcionalidade de forças de atrito está devidamente implementada. Se você quiser ver o resultado, adicione a seguinte linha de código:

HelloWorld::bolaBrancaCorpo->setVelocity(Vec2(-300.0,0.0));

logo abaixo dessa:

getEventDispatcher()->addEventListenerWithSceneGraphPriority(contato,this);

Compile o código e execute o jogo. Será simulada uma tacada na bola branca fazendo ela se movimentar em direção às outras duas bolas. Perceba que todas as bolas entram em repouso após a movimentação. Caso não implementássemos o tutorial de hoje, como não haveria forças de atrito, as bolas nunca parariam de rolar. Para os curiosos de plantão, segue o vídeo da simulação física da tacada.


Implementamos no tutorial de hoje a aplicação de forças de atrito nas bolas de sinuca. Vimos que o motor físico do Cocos2d-x não nos ajudava muito nessa tarefa. Precisamos implementar a detecção de colisão entre as bolas. Vimos também que a resposta das colisões nada mais é do que a aplicação das forças de atrito nos corpos rígidos colidentes. Além disso, para que as bolas parassem quando entrassem em repouso, é preciso programar a cessão das forças de atrito. Por último, nós realizamos uma tacada na bola branca para ver o resultado.

Existem formas mais eficientes computacionalmente de implementar jogos de sinuca. Nós estamos utilizando o motor físico do Cocos2d-x para realizar a movimentação das bolas na mesa. Mas podemos também implementar toda a movimentação por meio da aplicação do movimento retilíneo uniformemente variado em combinação com a segunda lei de Newton. Essa é uma forma um pouco mais difícil de implementar e a deixarei como um desafio para vocês leitores. =]

Implementaremos no próximo tutorial o sistema de tacadas na bola branca.

Um grande abraço e até mais.

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.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *