PUCRS/FACIN
Curso
de Especializaçã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.