PUCRS/FACIN
Curso de Especiali
zação em Desenvolvimento de Jogos Digitais

Computação Gráfica 2D

Profs.
    Márcio Sarroglia Pinho
    Isabel Harb Manssour


Fisica com a BOX 2D
Aula I

O objetivo desta aula é iniciar o estudo da biblioteca BOX2D e sua integração com o Framework utilizado no curso.
O manual da Box2D pode ser encontrado na página http://www.box2d.org/manual.html
Os slides utilizados na aula sobre Física estão disponívels no Moodle e nestes links:
    Slides sobre física
    Slides sobre a Box2D

Integração da Box2D com o Framework

    Toda a integração entre a Box2D e o Framework que vem sendo utilizado no curso é realizado através da classe CPhysics, cuja definição é apresentada abaixo:
class CPhysics
{
    public:
           b2Body* newBoxImage(int id, CImage* image, float density, float friction, float restitution, float linearDamping, float angularDamping, bool staticObj=false);
        b2Body* newBox(int id, float x, float y, float width, float height, float rotation, float density, float friction, float restitution, float linearDamping, float angularDamping, bool staticObj=false);
        b2Body* newCircleImage(int id, CImage* image, float density, float friction, float restitution, float linearDamping, float angularDamping, bool staticObj=false);
        b2Body* newCircle(int id, float x, float y, float radius, float density, float friction, float restitution, float linearDamping, float angularDamping, bool staticObj=false);


        void setImage(b2Body* body, CImage* sprite);
        CImage* getImage(b2Body* body);
        void setColor(b2Body* body, const b2Color& cor);
        b2Color& getColor(b2Body* body);
        void setPosition(b2Body* body, const b2Vec2& pos);
        void setAngle(b2Body* body, float angle);
        void setGravity(float grav);
        float getGravity();
        void setId(int id);
        int getId();
        void step();
        void debugDraw();
        bool haveContact(int id1, int id2);
        void setDrawOffset(float ox, float oy);
        static void setConvFactor(float conv);
        // Implement Singleton Pattern
        static CPhysics* instance()
        {
            return &m_CPhysics;
        }
    protected:
        CPhysics();
        ~CPhysics();

};

Inicializando a Biblioteca de Física

Para iniciar o uso da BOX2D é necessário obter um ponteiro para um objeto da classe CPhysics:

   
CPhysics *Fisica;
    Fisica = CPhysics::instance();

A seguir, deve-se inicializar a gravidade a ser usada no jogo, através de um vetor.

    b2Vec2 g(0,10);
    Fisica->setGravity(g);

A BOX2D, de acordo com sua documentação, foi pensada para trabalhar em um espaço entre 0 e 100 metros, logo, se seu cenário é muito maior ou menor do que isto, é necessário utilizar um fator de conversão que irá DIVIDIR as coordenadas de sua aplicação de forma que estas se encaixe no espaço de coordenadas da Box2D.
No caso de nosso exemplo, os objetos estão em um espaço de coordenadas entre 0 e 1000 aproximadamente, o que nos leva a uma fator de conversão de 10, que deve ser setado como segue:

    Fisica->setConvFactor(10);

Primeiro Teste

Baixe do Moodle a última versão do Framework e abra no Visual Studio.
Rode o programa.

Abra o fonte PlayFisicaState.cpp.

No método ::InitFisica faça a inicialização da biblioteca de física, conforme o exemplo abaixo.

    // inicializa a classe de física e a Box2D
    Fisica = CPhysics::instance();
    b2Vec2 g(0,0); // inicialmente não teremos gravidade
    Fisica->setGravity(g);
    Fisica->setConvFactor(10);

A variável Fisica está definida na classe PlayFisicaState.

A seguir, vamos iniciar a associação dos objetos da física com os objetos do Framework.
Para tanto, utilizaremos o método
newBoxImage da classe CPhysics, conforme o exemplo abaixo. Observe os parâmetros do método.

    CSprite *s;
    s = spriteCao;
    fisicaCao = Fisica->newBoxImage(CAO_ID,    //int id,  --> definida no arquivo PlayFisicaState.h
                                    s,                 // CImage* sprite,
                                    1,                 // float density,
                                    1.0,              // float friction,
                                    0.0,              // float restitution
                                     0.5,             // float linearDamping
                                     0.5,             // float angularDamping
                                    false);          // bool staticObj=false


Copie este código para o método InitFisica, logo após a inicialização feita acima.

O objeto
fisicaCao está declarado na classe PlayFisicaState, da seguinte forma:

        b2Body* fisicaCao;

O tipo b2Body, está definido na Box2D, e sua documentação pode ser acessada nesta página ou no manual da Box2D.

Ao final do método PlayFisicaState::Update coloque o código dado a seguir. Este código serve para obter as coordenadas do objeto durante a simulação física.

    b2Vec2 pos;
    pos = fisicaCao->GetPosition();
    cout << "X = "<< pos.x << " Y = " << pos.y << endl;

O tipo b2Vec2 é usadao pela Box2D apra armazenar pontos e vetores.
Observando a janela de console você irá notar que a coordenada devolvida pelo método é uma coordenada da biblioteca de física e não a coordenada da sprite, pois esta variável é uma variável da Box2D e não do Framework.

Se desejar obter a coordenada da sprite use os métodos próprios da sprite ou então multiplique a coordenada obtida pela GetPosition pelo fator de escala informado no método PlayFisicaState::InitFisica()
, através do comando Fisica->setConvFactor(10).

Atualizando a posição das Sprites

Para fazer com que as sprites movam-se como objetos da física, temos que ativar a atualização da simulação através do método

    
Fisica->step();

Coloque este código ao final do método
PlayFisicaState::update, antes da impressão das coordenadas do objeto.

Exibindo a "Caixa de Colisão da Sprite"

Adicione ao final do método Draw a chamada do método

    Fisica->debugDraw();

Tome o cuidado de colocar esta chamada antes da chamada da função SDL_GL_SwapBuffers.

Esta chamada irá exibir um retângulo semi-transparente ao redor das sprites. Isto será útil para entendermos melhor como ocorrem as colisões entre os objetos controlados pela Box2D.

Movendo um Objeto

Para mover um objeto controlado pela física da Box2D é preciso aplicar forças a ele. Para tanto inicialmente usaremos o método ApplyLinearImpulse para aplicar um impulso horizontal.

No método handleEvents, adicione, dentro do case SDL_KEYDOWN,  o tratamento de mais uma tecla, conforme o exemplo abaixo.


if  (event.key.keysym.sym == SDLK_l)
{

                b2Vec2 impulso;
                b2Vec2 pos;
                impulso.x = 100;   impulso.y = 0; // define um impulso horizontal
                pos = fisicaCao->GetWorldCenter(); // obtém as coordenadas do centro de massa do objeto
                pos.y +=-2;
                fisicaCao->ApplyLinearImpulse(impulso, pos);
                break;
}

O método
ApplyLinearImpulse recebe um vetor que define a direção e a força do impulso. A força é dada pelo módulo(tamanho) do vetor e a direção por suas coordenadas X e Y.

Rode o programa e teste o resultado pressionando a tecla "l".

Altere os parâmetros linearDamping e angularDamping na chamada do método newBoxImage no método PlayFisicaState::InitFisica. Teste valores entre 0 e 10.
Estes parâmetros define o "amortecimento" dos movimentos de translação e
rotação, respectivamente.

Experimente alterar a direção do impulso, aplicando por exemplo o vetor

        b2Vec2 impulso(0,100);

Experimente alterar o tamanho do vetor, aplicando, por exemplo, o vetor

        b2Vec2 impulso(0,1000);

Ponto de Aplicação dos Impulsos

O impulso dado ao objeto nos exemplos acima foi aplicado exatamente sobre seu centro de massa, obtido pelo método GetWorldCenter. Em função disto, ocorreu apenas um deslocamento no objeto.

A caso a força seja aplicada fora do centro de massa, será produzida
uma rotação no objeto.  Na figura abaixo observa-se a diferença do ponto de aplicação do impulso.



Para testar, mude a coordenada Y da variável pos, no trecho de código acima, fazendo, por exemplo:


                pos = fisicaCao->GetWorldCenter(); // obtém as coordenadas do centro de massa do objeto
                pos.y += 2;
                fisicaCao->ApplyLinearImpulse(impulso, pos);

Faça outros testes aplicando impulsos em outros pontos do objeto.

Criando novos Objetos

Além de criar obejtos vinculados a sprites ou  imagens, é possível adicionar objetos cujas dimensões são definidas através de valores numéricos fornecidos como parâmetros no momento da criação do objeto. Para criar este tipo de objeto, existe o método CPhysics::newBox.

Adicione o trecho de código abaixo no método PlayFisicaState::InitFisica.

    Fisica->newBox(OBSTACULO1,    //int id,
                   s->getX() + s->getWidth()*2, //pos x
                   s->getY(),            // pos y
                   s->getWidth(),                // width
                   s->getHeight(),            // height
                   0,                // rotation
                   1,              // float density,
                   1.0,            // float friction,
                   0.0,            // float restitution
                   0.5,                // float linearDamping
                   0.5,                // float angularDamping
                   false);          // bool staticObj=false


Este trecho de código gera um objeto retangular com do mesmo tamanho da sprite já criada, porém posicionado à mais a direita na tela.
Teste o programa deslocando o sprite com o teclado.

A seguir, mova o obstáculo para baixo, e altere sua largura, como no trecho a seguir:

    Fisica->newBox(OBSTACULO1,    //int id,
                   s->getX() + s->getWidth()*2, //pos x
                   s->getY() + s->getHeight()/2,            // pos y
                   s->getWidth()/2,                // width
                   s->getHeight(),            // height
                   0,                // rotation
                   1,              // float density,
                   1.0,            // float friction,
                   0.0,            // float restitution
                   0.5,                // float linearDamping
                   0.5,                // float angularDamping
                   false);          // bool staticObj=false


Troque o último parâmetro por true e teste o programa. Esta é a forma de se criar um obstáculo.

Altere o parâmetro restitution para 1
e teste o programa.

Adicionando Círculos

Recoloque o úlimo parâmetro do obstáculo em false.
 
No método
PlayFisicaState::InitFisica, coloque o código abaixo e teste o programa.

    Fisica->newCircle(OBSTACULO1+1,
                      s->getX() + s->getWidth()*1.5,
                      s->getY(),
                      15,                // float radius
                      1,              // float density,
                      1.0,            // float friction,
                      0.0,            // float restitution
                      0.5,            // float linearDamping
                      0.5,            // float angularDamping
                      false);         // bool staticObj=false

Este método cria um círculo de raio 15 posicionado entre o sprite e o obstáculo.

Recoloque o último parâmetro do obstáculo que está à direita em true. Mude o valor do parâmetro restitution dos objetos do programa.

Desenhado Vetores de Força

Em algumas aplicações que utilizam física é necessário exibir os vetores de força que não aplicados aos objetos.
Para desenhar o vetor de impulso que está sendo aplicado ao sprite deste exercício, inicialmente,
acrescente a declaração de duas variáveis do tipo b2Vec2 na definição da classe PlayFisicaState (no arquivo .h). Estas variáveis irão armazenar o vetor de impulso a ser aplicado ao objeto.

        b2Vec2 Direcao;         // Vetor de impulso
        b2Vec2 PontoFinal;    // Ponto de aplicação do impulso

Coloque no final do método
PlayFisicaState::update() a atualização destes atributos, conforme o trecho de código a seguir.

         PontoFinal = fisicaCao->GetWorldCenter();
         Direcao = b2Vec2(1000,0);


Isto irá atualizar as coordenadas do vetor aplicado ao sprite.

Altere no método PlayFisicaState::handleEvents o trecho de código responsável pela aplicação do impulso no objeto, conforme o exemplo abaixo. Desta forma serão utilizadas as variáveis atualizadas no método
PlayFisicaState::update() .

            if  (event.key.keysym.sym == SDLK_l) {
                fisicaCao->ApplyLinearImpulse(Direcao, PontoFinal);
                cout << "aplicando impulso...."<< endl ;

                break;
            }

Rode o programa. Neste ponto ele deve continuar funcionando como anteriormente.

Vamos passar agora ao desenho do vetor aplicado ao objeto.

Acrescente à classe PlayFisicaState um método capaz de desenhar uma linha, conforme o exemplo a seguir:

void PlayFisicaState::DesenhaLinha(b2Vec2 pontoInicial, b2Vec2 pontoFinal) {
    glColor3f(0,0,0);
    glBegin(GL_LINES);
    {
        glVertex2f(pontoInicial.x, pontoInicial.y);
        glVertex2f(pontoFinal.x, pontoFinal.y );
    }
    glEnd();

    glColor3f(1,1,1);
}

Lembre-se de acrescentar o cabeçalho da função no arquivo
PlayFisicaState.h, conforme o exemplo a seguir.

void DesenhaLinha(b2Vec2 pontoInicial, b2Vec2 pontoFinal) ;

A seguir acrescente, , no método PlayFisicaState::draw, logo após a chamada do método debugDraw, o trecho abaixo, que faz o traçado de uma linha do canto superior esquerdo da tela, até o PontoFinal, que armazena o ponto de aplicação do impulso.
 


    b2Vec2 fim;    
    // converte de coordenadas da física para coordenadas de tela
    fim.x = PontoFinal.x * 10; 
    fim.y = PontoFinal.y * 10;
   
    b2Vec2 inicio;
    inicio.x = 0;
    inicio.y = 0;
   
    DesenhaLinha( inicio,  fim);

Rode o programa e veja o resultado. Deve ser semelhante ao que está na figura a seguir.



Note que o ponto PontoFinal está no canto superior esquerdo do sprite,
porém o que desejamos é que este seja o meio da sprite. O que ocorre neste caso é que as sprites tem seu ponto de origem (0,0) no canto superior esquerdo enquanto na Box2D esta origem está no meio do objeto, como pode ser observado a figura abaixo.



Para corrigir esta diferença
é preciso alterar a variável PontoFinal, somando a ela a metade das dimensões da sprite, como no exemplo abaixo.

    b2Vec2 fim;    
    // converte de coordenadas da física para coordenadas de tela
    fim.x = PontoFinal.x * 10;
    fim.y = PontoFinal.y * 10;
    fim.x = fim.x + spriteCao->getWidth()/2;
    fim.y = fim.y + spriteCao->getHeight()/2;



Agora, precisamos acertar o início do vetor. Para tanto precisamos do ponto no centro da sprite o vetor de impulso, conforme o código dado a seguir.
   
    b2Vec2 inicio;
    inicio.x = fim.x - Direcao.x;
    inicio.y = fim.y - Direcao.y;
    DesenhaLinha( inicio,  fim);

O resultado, pode ser visto na figura a seguir.



Caso seja necessário rotacionar o vetor de impulso, você pode utilizar as fórmulas de rotação disponíveis nesta página, lembrando que a rotação no caso de um cenário 2D, ocorre ao redor do eixo Z.

Colisões

Para determinar a existência de colisões entre dois objetos definidos na Box2D, pode-se usar o método CPhysics::haveContact, para o qual deve ser passados os identificadores os objetos dos quais se deseja saber se há colisão.
 

    if (Fisica->haveContact(OBSTACULO1, CAO_ID))
    {
        cout << "Contato !!!"<< endl;
    }

Colisão com Mapa de Tiles

Um dos usos mais comuns de mapas de tiles é fazer deles mapas de colisão. Para, podemos utilizar um código como no exemplo abaixo. Note nas linhas assinaladas em azul, no método PlayFisicaState::CriaMapDeColisao que é preciso ajustar manualmente a posição do objeto a ser criado na Box2D, em função da diferença de localização do ponto (0,0) entre a sprite e os oejtos da Box2D. Este método deve ser chamado na PlayFisicaState::InitFisica.

Para fazer a criação efetiva dos objetos na Box2D foi criada um método
PlayFisicaState::CriaCaixa, que simplifica o processo, em especial no que diz respeito aos parâmetros. 


// Cria uma caixa de colisão
void PlayFisicaState::CriaCaixa(int id, float x, float y, float w, float h) {
    Fisica->newBox(id, x,y,w,h,
                   0,                // rotation
                   1,              // float density,
                   1.0,            // float friction,
                   0.0,            // float restitution
                   0.5,            // float linearDamping
                   0.5,            // float angularDamping
                   true);          // bool staticObj=false

}

void PlayFisicaState::CriaMapDeColisao() {
    float x,y; // usadas para calcular a posição de cada "tile"
    x = 0;
    y = 0;
    int identificador = BOLA + 5; // identificador para cada tile
    float width = mapColisao->getWidth();
    float height = mapColisao->getHeight();
    float difX = width/2;
    float difY = height/2;

    for(int lin=0; lin < mapColisao->getHeightTileMap(); lin++) {
        for (int col=0; col< mapColisao->getWidthTileMap(); col++) {
            //cout <<  "   "  << mapColisao->getTileNumber(col,lin);
            if (mapColisao->getTileNumber(col,lin) != 192) { // se não é um "tile" do fundo
                // faz manualmente o ajuste entre o (0,0) do "tile" e o (0,0) da Box2D
                CriaCaixa(identificador, x+difX,y+difY, width, height);
                identificador ++;
                cout << "["<< col << "," << lin << "]  --  X = "<< x << " Y = " << y << endl;
            }
            //else cout << "["<< col << "," << lin << "] --" << endl;
            x += mapColisao->getWidth();
        }
        //cout << endl;
        x = 0;
        y += mapColisao->getHeight();
    }

}





No exemplo deste código, todos os objetos criados tem as mesmas características físicas, mas isto não é obrigatório. Uma possibilidade é usar o valor do tile, obtido na chamada
mapColisao->getTileNumber(col,lin), para definir objetos com características físicas distintas.

Exercício

Monte um cenário semelhante a uma mesa de bilhar e faça testes para montar um jogo deste estilo.

-----
FIM.