set-18-2009

Passive VS Active Rendering – parte 2 de 2

FullScreenVimos no post anterior a atualização passiva e suas limitações. Vejamos agora coisa real, criando um programa com maior previsibilidade na atualização da tela, capacidade de rodar em tela-cheia (fullscreen) e de mudar o modo de vídeo.  Para isso aprenderemos a atualização ativa (active rendering).

Atualização ativa

No artigo anterior vimos a atualização passiva que pode ser utilizada para a criação de jogos que possuem gráficos simples ou ação lenta. E nestes casos é até recomendada, porque compromete pouco a CPU. Mas em jogos mais animados não podemos depender do S.O. para gerar as atualizações da tela. Nesse caso desligamos o mecanismo de atualização automatica e partimos para o manual, com a atualização ativa.

O Java nos permite controlar a tela de forma deterministica por meio da classe BufferStrategy. Não é um acesso direto à placa de vídeo, porque o Java nos isola da camada de hardware. Mas isso é bom, porque simplifica a coisa e nos oferece uma forma padrão para trabalhar, o mais independente possível da configuração da máquina do usuário.

Abaixo mostro o código de um programa quase idêntico ao do artigo anterior, porém agora usando atualização ativa.

Arquivo: Main.java

 1 package abrindoojogo.exemplos.atualizacaoativa;
 2
 3 public class Main
 4 {
 5     public static void main(String[] args)
 6     {
 7         MeuJogo mj = new MeuJogo();
 8         mj.setVisible(true);
 9         mj.initialize();
10     }
11 }
12
13

Veja que a classe principal é quase igual. A diferença é que a classe MeuJogo agora possui um método initialize que deve ser chamado depois de tornarmos ela visível. Nesse método inicializamos a BufferStrategy (veja abaixo). Destaquei em vermelho as partes que são novidade em relação ao exemplo anterior.

Arquivo: MeuJogo.java

 1 package abrindoojogo.exemplos.atualizacaoativa;
 2
 3 import java.awt.Color;
 4 import java.awt.Graphics2D;
 5 import java.awt.event.KeyEvent;
 6 import java.awt.event.KeyListener;
 7 import java.awt.image.BufferStrategy;
 8 import javax.swing.JFrame;
 9 import javax.swing.WindowConstants;
10
11 public class MeuJogo extends JFrame implements KeyListener
12 {
13     int tom;
14     BufferStrategy bs;
15
16     public MeuJogo()
17     {
18         setTitle("Atualização Ativa");
19         setSize(800, 600);
20         setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
21         setIgnoreRepaint(true);
22         addKeyListener(this);
23         tom = 0;
24     }
25
26     public void initialize()
27     {
28         createBufferStrategy(1);
29         bs = getBufferStrategy();
30         render();
31     }
32
33     public void render()
34     {
35         Graphics2D g = (Graphics2D) bs.getDrawGraphics();
36         g.clearRect(0, 0, getWidth(), getHeight());
37         for (int x = 0; x < 800; x += 10)
38         {
39             for (int y = 0; y < 600; y += 10)
40             {
41                 g.setColor(new Color(x % 256, y % 256, (tom * 50)
                   % 256));
42                 g.fillRect(x, y, 10, 10);
43             }
44         }
45         g.dispose();
46         bs.show();
47     }
48
49     public void keyPressed(KeyEvent e)
50     {
51         if (e.getKeyCode() == KeyEvent.VK_ENTER)
52         {
53             tom++;
54             render();
55         }
56     }
57
58     public void keyReleased(KeyEvent e)
59     {
60     }
61
62     public void keyTyped(KeyEvent e)
63     {
64     }
65 }
66
67

O contructor é muito parecido, mas preste atenção. Ele inclui agora uma chamada ao método setIgnoreRepaint() passando o valor true. Isso desliga a atualização automática. O S.O. não vai mais gerar eventos de atualização para nossa janela.

Em seguida temos o método initialize().

26     public void initialize()
27     {
28         createBufferStrategy(1);
29         bs = getBufferStrategy();
30         render();
31     }

Nesse método eu chamo createBufferStrategy(), que é pertencente à classe JFrame. O parâmetro é a quantidade de buffers de tela que quero usar. Falarei sobre isso adiante. Adquiro uma referência ao BufferStratey e armazeno na variável bs. Através desta variável eu vou ter acesso ao buffer da tela para desenhar meus gráficos. Logo após chamo o método render().

O método render() é o próximo declarado na classe. É nele que se dá o desenho da tela. Veja que é praticamente o mesmo código que estava dentro do método paint() do artigo anterior. Só que agora é um método próprio e não um manipulador de evento. Lembre que desligamos a atualização automática, que chamava o paint(). O método render(), por sua vez, será chamado diretamente por mim, quando eu desejar que a tela seja redesenhada.

33     public void render()
34     {
35         Graphics2D g = (Graphics2D) bs.getDrawGraphics();
36         g.clearRect(0, 0, getWidth(), getHeight());
37         for (int x = 0; x < 800; x += 10)
38         {
39             for (int y = 0; y < 600; y += 10)
40             {
41                 g.setColor(new Color(x % 256, y % 256, (tom * 50)
                   % 256));
42                 g.fillRect(x, y, 10, 10);
43             }
44         }
45         g.dispose();
46         bs.show();
47     }

Examinando esse método, vemos que a parte que lida com o objeto “g”, do tipo Graphics2D, é idêntica à anterior. A diferença está no modo como obtemos o Graphics. Antes ele era recebido como parâmetro no método paint(). Ou seja, o S.O. nos passava o Graphics ao chamar o método. Agora nós pegamos um Graphics do objeto BufferStrategy. Esse Graphics que ele retorna é uma referência para o buffer da tela, desenhando nele, desenhamos nela. Depois de usar, chamamos o método dispose para liberar – adquirimos e liberamos um novo Graphics a cada render.

Depois do desenho pronto, chamamos o método show() do BufferStrategy. Embora nesse momento ele fique um pouco perdido, vai fazer mais sentido quando eu falar em mais de um buffer, daqui a pouco.

A modificação final é no método keyPressed(), que é o manipulador para o evento de tecla pressionada. Depois de atualizar a variável “tom”, chamamos render(). Aqui reside a grande diferença: enquanto antes chamavamos repaint() para avisar o S.O. que desejávamos uma atualização, e ficava a cargo dele atualizar a tela quando bem entendesse, agora chamamos diretamente nosso método render(), que vai obter um Graphics e desenhar na hora, sem espera.

Execute esse programa e o resultado vai ser parecido com o anterior, na parte visual. Quando você ficar pressionando ENTER a tela vai piscar da mesma forma, porém a atualização da tela consome mais CPU e por isso se você ficar pressionando por muito tempo, os eventos keyPressed não vão ser processados a tempo e vão acumular (ao soltar a tecla continuarão sendo gerados eventos por algum tempo).

Note também que redimensionando a janela ela não é mais redesenhada. Não há mais eventos relacionado a essa ação.

DoubleBuffer

Para evitar as piscadas na tela (o efeito chamado de tearing – rasgo), utilizaremos dois buffers ao invés de um. A lógica é a seguinte: se temos apenas um buffer, ele está sempre sendo mostrado na tela. Quando desenhamos nele, o desenho é feito diretamente nela e podemos ver o desenho sendo contruído e reconstruído repetidamente. Isso causa o efeito indesejado.

Se utilizarmos dois buffers, enquanto um deles é mostrado na tela, desenhamos no outro. Assim, enquanto o desenho está sendo contruído, nós estamos olhando para o desenho anterior, que está estático. Quando o novo desenho está pronto, o método show() do BufferStrategy troca os buffers. Aquele que acabamos de atualizar vai para a tela e o outro passa a ser o de desenho. E ficamos nessa, sempre desenhando no buffer de trás (back buffer) enquanto o outro (front buffer ou buffer primário) fica estático na tela. Como a troca de buffers é muito rápida, centenas de vezes mais rápida do que gerar um desenho na tela, não ocorre nenhum efeito perceptível para o usuário.

Para usar dois buffers, basta mudar o parâmetro do método createBufferStrategy(). Passe o número 2 ao invés de 1 e execute o programa. As piscadas sumirão. Você pode usar mais buffers, mas isso torna o processo mais lento e consome bem mais memória – veja, cada buffer é uma cópia da tela, seria como ter três telas na memória de vídeo. E memória de vídeo é algo que não podemos desperdiçar. Apenas em casos muito específicos, ou em determinados hardwares, o uso de triplebuffer traz melhorias e pode ser usado. Mais de três buffers, nem pensar – você pode até passar um número maior, mas provavelmente serão criados apenas 2 ou 3.

Observação: dependendo do seu hardware, a troca de buffers pode ser feita de formas diferentes, o que gera resultados melhores para uns do que para outros. De qualquer forma a diferença deste exemplo para o anterior será gritante.

Fullscreen e mudança de modo de vídeo

Que tal rodar o jogo em tela-cheia? Essa é melhor opção porque cria uma imersão muito mais profunda para o usuário, melhorando sua experiência com o jogo. Mas só tela-cheia não basta por causa do tamanho da tela. Se seu jogo foi feito em 800×600 (como é o caso desse exemplo), colocar em tela-cheia vai fazer ele não ocupar toda tela, ficando uma borda preta ao redor. Em um monitor com resolução de 1440, o jogo fica sendo apenas um quadro no meio da tela.

Para usar tela-cheia, o ideal, então, é mudar o modo de vídeo do monitor para ajustar-se à resolução do jogo. Mudando o modo de vídeo para 800×600 o jogo ocupa todo monitor, sem bordas.

Fazer isso é bem fácil. Veja abaixo o método initialize modificado para colocar a janela em tela-cheia e logo após mudar o modo de vídeo para 800×600.

28     public void initialize()
29     {
30         createBufferStrategy(1);
31         bs = getBufferStrategy();
32         getGraphicsConfiguration().getDevice().
           setFullScreenWindow(this);
33         getGraphicsConfiguration().getDevice().setDisplayMode(
           new DisplayMode(800, 600, 32,
           DisplayMode.REFRESH_RATE_UNKNOWN));
34         render();
35     }
 

Utilizei o método getGraphicsConfiguration() herdado da classe JFrame para, através dele, obter acesso ao dispositivo de vídeo (getDevice). A classe GraphicsDevice nos provê um método para entrar em tela-cheia, que é setFullScreenWindow(). Passamos para ele a janela que vai ocupar a tela – nesse caso, nossa própria classe, que é uma janela. Passando null para esse método saimos da tela-cheia, o que também ocorre, automaticamente, ao fecharmos o programa.

Outro método que temos à disposição é o setDisplayMode(). Passamos para ele um objeto do tipo DisplayMode, criado com os dados do modo de vídeo desejado. Os primeiros dois parâmewtros são largura e altura (800×600). Em seguida a profundidade de cor, ou seja, quantos bits por cor. No caso, passei 32 bits – hoje todos monitores suportam isso. O último parâmetro é a taxa de atualização do monitor (refresh rate). Por exemplo, pode ser 60, 70, 80, etc, onde 60 significa 60Hz e assim por diante. Mas essa taxa não é padronizada e por isso é melhor utilizar a constante DisplayMode.REFRESH_RATE_UNKNOWN, que não muda a taxa de atualização do monitor.

Executando o programa assim, ele entra ela tela-cheia. Mas ainda fica com a borda da janela, o que não é desejado nesse caso. Programas em tela-cheia não exibem a borda. Para esconder ela, basta chamar o método setUndecorated() passando true. Isso tira a decoração da janela (borda, título, botões). Veja abaixo como chamei esse método no contructor.

17     public MeuJogo()
18     {
19         setTitle("Atualização Ativa");
20         setSize(800, 600);
21         setUndecorated(true);
22         setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
23         setIgnoreRepaint(true);
24         addKeyListener(this);
25         tom = 0;
26     }

Pronto, agora temos um programa que roda em tela-cheia, mudando a resolução do vídeo e sem piscar ao atualizar a tela. Além de termos a garantia de que a tela vai ser atualizada sempre que mandarmos.

Mas para evoluir até um jogo, falta um detalhe importante! Você notou que a coisa só se “mexe” quando pressionamos ENTER. Isso porque é dentro do evento do teclado (keyPressed) que modificamos a variável e atualizamos a tela. Um jogo real fica modificando seu estado e atualizando a tela sozinho, sem ninguém pressionar um tecla. Para isso temos que ter um loop em algum lugar – o fatídico game loop. Assunto para o próximo post.

Até lá, pensem na melhor forma de fazer isso. Que tal fazer esse programa ficar mudando as cores sem estar o ENTER pressionado? Mãos à obra.

Baixe o código fonte deste artigo.

Postado em Java, Programação, Técnico
  1. Cissa Baini disse,

    Nao entendi nada, mas hein?!… Tá chique esse blog! Assssaááá, manos!

    Muito legal esta disponibilidade de interação, este espaço de conexão com o mundo. Eis a globalização praticada para bons fins!

    Lindo, lindos!
    PARABÉNS!

  2. jean patrick disse,

    Parabens pelo post, muitissimo interessante! Essa estratégia serve tbm para J2ME?

  3. Luiz Alessandro Nörnberg disse,

    Sim e não. De forma geral, sim, o conceito. Mas a implementação que mostrei não funciona no J2ME – ele não possui estas classes. Se o dispositivo para o qual você vai programar é compativel apenas com MIDP 1.0, não tem como fazer. Se é compativel com MIDP 2.0, já tem pronta a classe GameCanvas, que posui um método run() dentro do qual você faz seu game loop. Dai basta chamar a rotina getGraphics() para obter o objeto para desenho e depois de desenhar, chamar flushGraphics() para mostrar na tela. Vou mostrar isso em um post dentro de pouco tempo.

Deixe seu comentário