O objetivo do presente relatório é discutir como foi feita a implementação do jogo "resta 1". Para isto, adotou-se a seguinte metodologia: primeiro foi feito um levantamento geral sobre os problemas que tinham que ser resolvidos, como modelagem geométrica, interface com o usuário, a lógica do jogo e alguns detalhes na aparência final do aplicativo. E em segundo lugar, feito o levantamento dos problemas, foram propostas soluções para resolvê-los. As soluções preferenciais foram soluções com implementação mais simples e diretas. A implementação do jogo "resta 1" foi feita utilizando a linguagem C++ e a biblioteca gráfica OpenGL.
O relatório está organizado da seguinte forma: na seção 2 são feitos os levantamentos dos problemas a serem resolvidos. Na seção 3 são abordadas as soluções adotadas para a resolução dos problemas levantados na seção 2. E na seção 4, são feitos alguns comentários a respeito das dificuldades encontradas na implementação do jogo "resta 1".
Os problemas descritos nesta seção foram encontrados conforme a necessidade de implementação do jogo e suas soluções são descritas na seção 3. O primeiro nível de problema é referente à modelagem geométrica: como desenhar as peças, como modelar e desenhar o tabuleiro. A segunda classe de problemas engloba a interface com o usuário: como possibilitar o movimento das peças do jogo. O terceiro nível de problemas trata da parte lógica: quais movimetos são permitidos e quais não são, estes movimentos são determinados pelas regras do jogo, as quais permitem apenas movimentos na vertical e na horizontal. E finalmente, quais melhorias podem ser feitas para que o aplicativo fique com uma boa aparência visual.
Um dos maiores desafios na solução dos problemas descritos anteriormente é propor soluções simples e eficientes para os mesmos. Para isto, torna-se necessário o conhecimento das linguagens de programação que serão utilizadas, no caso OpenGL e C++, para que as soluções possam se tornem fáceis. Assim como um conhecimento razoável em técnicas de programação e um entendimento completo a respeito do domínio do problema que está sendo abordado. Nas subseções a seguir são descritos os problemas abordados na seção anterior e os respectivos códigos utilizados para solucioná-los.
Para as peças do jogo foi escolhido um formato cilíndrico e em sua extremidade foi colocado uma pequena esfera, para dar um forma arredondada a mesma. Para consegui esta forma foram utilizadas as seguintes funções do OpenGL:
void desenhar_peca() /*procedimento para desenhar as peças*/
GLfloat cor_peca[] = {.9, .8, .7, 0.}; GLUquadric * quad =
gluNewQuadric();
glMaterialfv(GL_FRONT, GL_DIFFUSE
,cor_peca);
glColor3f(1., 1., 1.);
gluCylinder(quad, .3, .3, .5, 10, 10);
glTranslatef(0., 0., .5);
gluSphere(quad, .3, 12, 12);
gluDeleteQuadric(quad);
O tabuleiro do jogo é composto por uma matriz 7x7, entretanto, nem todas as regiões dele são utilizadas. Para se conseguir o efeito desejado, foram utilizados três cubos com escalas diferentes posicionados nas mesmas coordenadas e sobrepostos entre si. Os efeitos das divisões nos cubos foram conseguidos com o traçado de linhas sobre os dois cubos superiores. O código abaixo descreve o desenho dos cubos e das linhas:
void linhas(float max, float x)
/*formatuo das linhas*/
glVertex3f(x, max*LARG, .01);
glVertex3f(x,
-max*LARG, .01);
glVertex3f(max*LARG,
x, .01);
glVertex3f(-max*LARG,
x, .01);
glBegin(GL_LINES);
/* looping para desenhar as linhas*/
for (float x = -3.5*LARG; x <= 3.5 *
LARG; x += LARG)
linhas(1.5,
x);
for (float x
= -1.5*LARG; x <= 1.5 * LARG; x += LARG)
linhas(3.5, x);
/* funções para desenhar os cubos e as peças */
glTranslatef(0., 0., -.25);
glScalef(LARG*3., LARG*7., .5);
glutSolidCube(1.);
/*cubo com linhas*/
glScalef(7./3.,
3./7., 1.);
glutSolidCube(1.); /*cubo com linhas*/
glTranslatef(0., 0., -.75);
glScalef(2.5, 2.5, .1);
glColor3f(.5, .5, .5);
glutSolidCube(5.); /*cubo sem linhas*/
glColor3f(0, 0, 0);
glBegin(GL_LINES);
for (float x
= -3.5*LARG; x <= 3.5 * LARG; x += LARG)
linhas(1.5,
x);
for (float x
= -1.5*LARG; x <= 1.5 * LARG; x += LARG)
linhas(3.5, x);
glEnd();
A Figura 1
mostra o formato do tabuleiro assim como as peças sobre o mesmo.
O que
procurou-se fazer nesta etapa foi facilitar a interativiade do usuário com o
aplicativo através de comandos de mouse e teclado. Para tal, foram
disponibilizadas operações de rotação sobre e aproximação/distanciamento do
tabuleiro, assim como a movimentação de peças.
Para a visualização do tabuleiro de diversos ângulos, foi utilizada a seguinte chamada de função:
gluLookAt(raio*cos(teta)*cos(fi), -raio*sin(teta)*cos(fi),
raio*sin(fi), 0., 0., 0., -sin(fi)*cos(teta), sin(fi)*sin(teta), cos(fi));
em que o usuário tem o controle sobre os ângulos fi e teta através do pressionamento do botão direito do mouse e movimentando-o ou utilizando as setas do teclado. O controle sobre a distância do tabuleiro é feito utilizando as teclas "+ " e " - ". Os trechos de código abaixo apresentam como isso foi feito no OpenGL:
void especial(int key, int x, int y) {
bool
redesenhar(true);
switch(key)
{
case
GLUT_KEY_LEFT: teta += .09; break;
case
GLUT_KEY_RIGHT: teta -= .09; break;
case
GLUT_KEY_UP: fi += .09; break;
case
GLUT_KEY_DOWN: fi -= .09; break;
...
} }
void teclado(unsigned char tecla, int x, int y) {
bool redesenhar(true);
switch
(tecla) {
...
case '+':
raio -= .5; break;
case '-':
raio += .5; break;
...
} }
void mouse(int button, int state, int x, int y) {
switch
(button) {
case
GLUT_RIGHT_BUTTON:
if (rodando
= state == GLUT_DOWN) {
xant
= x;
yant =
y;
break;
...
} } }
void movimento(int x, int y) {
if
(rodando) {
fi +=
(y-yant)/500.;
teta += (x-xant)/500.;
} }
Para a movimentação das peças, quando o mouse fosse pressionado, havia a necessidade
de determinar sua posição no espaço para que fosse possível checar se o mesmo
estava sendo pressionado sobre alguma peça. Para conseguir isto, foi utilizada
a função gluUnProject, que mapeia um ponto do volume canônico para o
espaço, e a função glReadPixels para a leitura da coordenada " z " (no
volume canônico) do ponto " x, y " sobre o qual o mouse foi pressionado. O trecho
abaixo mostra a função de mapeamento:
void mapear(int x, int y, double& x1, double&
y1, double& z1) {
GLfloat z;
glReadPixels(x, H-y-1, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &z);
gluUnProject (x, H-y-1, z,
matrizModelview, matrizProjecao, matrizViewport, &x1, &y1, &z1);
}
A " lógica " do jogo foi implementada em dois arquivos separados da interface, jogo.cpp e jogo.hpp, para que os dois, interface e lógica, tivessem o mínimo de acoplamento possível. Com isso, permite-se que a lógica do jogo seja utilizada por outras interfaces e que a interface gráfica desenvolvida possa utilizar outras lógicas, dependendo da aplicação. No trecho de código mostrado abaixo, é feito um comentário sobre como está implementada a parte lógica. O código completo pode ser encontrado em: código fonte. Para isto, foi necessário um conhecimento sobre as regras do jogo " resta 1 ". As regras básicas são as seguintes: as peças só podem se mover na horizontal e na vertical, as peças só podem de movimentar de duas em duas casas, desde que exista uma peça na frente e a proxima casa esteja vazia, ou seja, as jogadas válidas são{x+2, x-2, y+2, y-2}, existem também algumas posições no tabuleiro que nao sao válidas. O jogo é iniciado com a posição central do tabuleiro vazia.
A função char inicial[7][8]{},
/*inicializa o tabuleiro com as seguintes características: "#" indica posição inválida,
"P" indica Peça, "B" posição em Branco " sem peça "*/
void reiniciar(); /*reinicia o jogo*/
bool pode_mover(int dx, int dy, int px, int py);
/*verifica se a peça pode ser movevida de (dx, dy) para (px, py)*/
bool pode_origem(int i, int j);
/*verifica se a posição (i,j) é uma origem de movimento válida*/
bool mover(int dx, int dy, int px, int py);
/*se for possivel, executa o movimento de (dx,dy) para (px,py)*/
bool operator()(int i, int j);
/*verifica se a posicao (i,j) tem uma peça*/
bool valida(unsigned i, unsigned j);
/*verifica se a posição (i,j) é uma posição válida*/
bool Jogo::pode_mover(int dx, int dy, int px, int py);
/*Verifica se a jogada de (dx,dy) para (px,py) é válida. É assumido que a posição (dx,dy)
já tenha sido verificada com "pode_origem".*/
bool Jogo::mover(int dx, int dy, int px, int py);
/*Verifica o movimento de (dx,dy) para (px,py), e o executa caso seja possivel,
atualizando o tabuleiro.*/
bool Jogo::pode_jogar();
/*// Verifica se o jogador ainda tem algum movimento possivel, no pior
caso (quando o jogador nao tem mais movimentos) testa todas as possibilidades de jogada.*/
Para a renderização de imagens mais agradáveis do tabuleiro e das peças, optou-se por disponibilizar iluminação difusa, posicionando o ponto de luz sobre o tabuleiro. A utilização ou não da mesma fica a critério do usuário, que pode habilitar/desabilitar pressionando a tecla " l". Os trechos abaixo mostram como foi implemenetada a iluminação no jogo:
GLfloat luz[] = { 1.0, 1.0, 1.0, 0.0};
GLfloat pos1[] = { 0., 0., 10., 0.};
...
glLightfv(GL_LIGHT1, GL_DIFFUSE, luz);
glLightfv(GL_LIGHT1, GL_POSITION, pos1);
O OpenGL mostrou-se uma ferramenta eficiente para a renderização de cenas,
implementando e disponibilizando operações complexas como a iluminação e a
transformação de projeção. Por implementar as operações de acordo com a teoria
abordada na computação gráfica, o mesmo torna-se intuitivo para quem conhece a mesma.
Apesar de mostrar-se uma ferramenta poderosa para a renderização de cenas, o OpenGL
também mostrou-se uma ferramenta pobre para a interação com o usuário, mesmo
utilizando em conjunto com bibliotecas como o GLU e o GLUT. O mesmo pôde ser
observado pelas dificuldades encontradas para a implementação da interação com
usuário, onde era necessário disponibilizar um \emph{picking} 3D.