Passive VS Active Rendering – parte 2 de 2

placeholderantigo

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.

Autor: Luiz Nörnberg Ver todos os posts de
Sou Bacharel em Ciência da Computação pela Universidade Católica de Pelotas (UCPel), onde também atuei como professor. Desde a época da faculdade (mais de quinze anos atrás) a paixão por jogos tem sido importante no meu direcionamento profissional. Sou sócio-fundador do Izyplay Game Studio, onde exerço o cargo de Diretor de Tecnologia. Sempre tive grande foco em desenvolvimento em Java, embora tenha migrando para a tecnologia Adobe AIR em função de sua portabilidade. Ah, e é claro, dou meus palpites no game design.

4 Comentários em "Passive VS Active Rendering – parte 2 de 2"

  1. Cissa Baini 18/09/2009 at 20:16 - Reply

    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 22/10/2009 at 22:55 - Reply

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

    • Luiz Alessandro Nörnberg 23/10/2009 at 00:39 - Reply

      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.

  3. Artur Renan 06/01/2011 at 22:42 - Reply

    Muito bom o artigo. Bem didático. Já tinha lido sobre o assunto no site da Oracle, mas não tinha a didática e explicações que encontrei aqui. Parabéns.

Deixar um Comentário