41bb726c

Dia 11

Mais animação, imagens e som

Laura Lemay


CONTEÚDOS

A animação é divertimento e fácil fazer em Java, mas há só tanto pode fazer com os métodos de Java construídos de linhas e fontes e cores. Para a animação realmente interessante, tem de fornecer as suas próprias imagens para cada armação da animação - e sons têm é bonito, também. Hoje fará mais com a animação, incorporando imagens e sons em Java applets.

Especificamente, explorará os seguintes tópicos:

Recuperar e usar imagens

O manejo de imagem básico em Java é fácil. A classe de Image no pacote de java.awt fornece métodos abstratos para representar o comportamento de imagem comum, e os métodos especiais definidos em Applet e Graphics dão-lhe tudo que tem de carregar e expor imagens no seu applet tão facilmente como desenho de um retângulo. Nesta seção, aprenderá sobre como adquirir e desenhar imagens no seu Java applets.

Obtenção de imagens

Para expor uma imagem no seu applet, primeiro deve carregar aquela imagem sobre a Rede no seu programa Java. As imagens guardam-se como arquivos separados dos seus arquivos de classe de Java, portanto tem de dizer Java onde encontrá-los.

A classe de Applet fornece um método chamado getImage(), que carrega uma imagem e automaticamente cria um exemplo da classe de Image para você. Para usá-lo, tudo que tem de fazer é importar a classe de java.awt.Image para o seu programa Java, e logo dar a getImage o URL da imagem que quer carregar. Há dois modos de fazer o último passo:

Embora o primeiro caminho possa parecer mais fácil (somente ligam o URL como um objeto de URL), o segundo é mais flexível. Lembre-se, porque compila arquivos de Java, se incluir um URL codificado de uma imagem e logo deslocar os seus arquivos a uma posição diferente, tem de recompilar todos os seus arquivos de Java.

A última forma, por isso, é normalmente aquela para usar. A classe de Applet também fornece dois métodos que ajudarão com o argumento de url básico a getImage():

Se usa getDocumentBase() ou getCodebase() depende de se as suas imagens são quanto aos seus arquivos de HTML ou quanto aos seus arquivos de classe de Java. Use tudo o que cada um apresenta-se melhor à sua situação. Observe que destes métodos é mais flexível do que a codificação difícil um URL ou nome de caminho no método de getImage(); usar getDocumentBase() ou getCodeBase() permite-lhe mover os seus arquivos de HTML e applets em volta e Java ainda pode encontrar as suas imagens. (Isto supõe, naturalmente, que mova os arquivos de classe e as imagens em volta em conjunto. Se mover as imagens em outro lugar e deixar os arquivos de classe onde são, terá de editar e recompilar a sua fonte.)

Aqui estão alguns exemplos de getImage, para dar-lhe uma ideia de como usá-lo. Esta primeira chamada a getImage() recupera o arquivo naquele URL específico (http://www.server.com/files/image.gif). Se qualquer parte do qual URL se modifica, tem de recompilar o seu Java applet para considerar o novo caminho:

Image img = getImage(
    new URL("http://www.server.com/files/image.gif"));

Na seguinte forma de getImage, o arquivo de image.gif está no mesmo diretório que os arquivos de HTML que se referem a este applet:

Image img = getImage(getDocumentBase(), "image.gif")

Nesta forma semelhante, o arquivo image.gif está no mesmo diretório que o próprio applet:

Image img = getImage(getCodeBase(), "image.gif")

Se tiver muitos arquivos de imagem, é comum pô-los no seu próprio subdiretório. Esta forma de getImage() procura o arquivo image.gif no diretório images, que, à sua vez, está no mesmo diretório que Java applet:

Image img = getImage(getCodeBase(), "images/image.gif")

Se getImage() não puder achar o arquivo indicado, devolve null. drawImage() em uma imagem de null simplesmente não desenhará nada. Usar uma imagem de null de outros modos causará provavelmente um erro.

Observar
Atualmente, Java apoia imagens em formatos de JPEG e o GIF. Outros formatos de imagem podem estar disponíveis depois; contudo, por agora, as suas imagens devem estar em GIF ou em JPEG.

Desenho de imagens

Todo aquele material com getImage() faz nada exceto se vai e recupera uma imagem e a enche em um exemplo da classe de Image. Agora que tem uma imagem, tem de fazer algo com ela.

Nota técnica
De fato, o carregamento de imagens é interiormente muito mais complexo do que isto. Quando recupera uma imagem usando getImage(), aquele método de fato cria um fio para carregar a imagem e regressos quase imediatamente com o seu objeto de Image. Isto dá ao seu programa a ilusão de quase instantaneamente ter a imagem lá pronta para usar. Pode levar um tempo, contudo, para a imagem real para carregar e descomprimir, que pode fazer que à sua imagem applets desenhe com imagens só parciais, ou para a imagem a desenhar-se na tela com incremento como carrega (todos os exemplos neste trabalho de capítulo como isto). Pode controlar como quer que o seu applet se comporte dado uma imagem parcial (por exemplo, se quiser que ele espere até que o esteja expondo tudo lá antes) aproveitando-se da interface de ImageObserver. Aprenderá mais sobre ImageObserver depois nesta lição na seção "Uma Nota sobre Observadores de Imagem".

A coisa mais provável que vai querer fazer com uma imagem é exposição ela como ia um retângulo ou uma cadeia de texto. A classe de Graphics fornece dois métodos para fazer somente isto, ambos chamaram drawImage().

A primeira versão de drawImage() toma quatro argumentos: a imagem para expor, o x e as posições de y do topo deixou a esquina e this:

public void paint() {
    g.drawImage(img, 10, 10, this);
}

Esta primeira forma faz a que o esperaria: desenha a imagem nas suas dimensões originais com a esquina deixada o topo no x dado e posições de y. A listagem 11.1 mostra o código de um applet muito simples que carrega uma imagem chamada ladybug.gif e o expõe. A figura 11.1 mostra o resultado óbvio.

A figura 11.1: A imagem de defeito de senhora.


A listagem 11.1. A Joaninha applet.
 1:import java.awt.Graphics;
 2:import java.awt.Image;
 3:
 4:public class LadyBug extends java.applet.Applet {
 5:
 6:    Image bugimg;
 7:
 8:    public void init() {
 9:       bugimg = getImage(getCodeBase(),
10:          "images/ladybug.gif");
11:    }
12:
13:    public void paint(Graphics g) {
14:      g.drawImage(bugimg, 10, 10,this);
15:    }
16:}

Neste exemplo a variável de exemplo bugimg mantém a imagem de joaninha, que se carrega no método de init(). O método de paint() então desenha aquela imagem na tela.

A segunda forma de drawImage() toma seis argumentos: a imagem para desenhar, o x e as coordenadas de y da esquina deixada o topo, uma largura e a altura da caixa delimitadora de imagem e this. Se os argumentos de altura e largura pela caixa delimitadora forem mais pequenos ou maiores do que a imagem real, a imagem escala-se automaticamente para ajustar-se. Usando aqueles extra argumentos, pode apertar e estender imagens em qualquer espaço ao qual precisa deles para ajustar-se (tenha em mente, contudo, que pode haver alguma degradação de imagem de escalá-lo mais pequeno ou maior do que o seu tamanho desejado).

Uma insinuação útil para escalar imagens é descobrir o tamanho da imagem real que carregou, portanto então pode escalá-lo a uma percentagem específica e evitar o torcimento em qualquer direção. Dois métodos definidos para a classe de Image podem dar-lhe que informação: getWidth() e getHeight(). Ambos tomam um argumento único, um exemplo de ImageObserver, que se usa para seguir a pista do carregamento da imagem (mais sobre isto depois). A maior parte do tempo, pode usar somente this como um argumento a getWidth() ou a getHeight().

Se guardou a imagem de joaninha em uma variável chamada bugimg, por exemplo, esta linha devolve a largura daquela imagem, em pixéis:

theWidth = bugimg.getWidth(this);

Nota técnica
Aqui está outro caso onde, se a imagem não se carregar durante todo o tempo, pode adquirir resultados diferentes. Chamando getWidth() ou getHeight() antes que a imagem tenha carregado totalmente resultará em valores de -1 para cada um. Seguir a pista de carregamento de imagem com observadores de imagem pode ajudá-lo a guardar a pista de quando esta informação aparece.

A listagem 11.2 mostra outro uso da imagem de joaninha, esta vez escalado várias vezes a tamanhos diferentes (a Figura 11.2 mostra o resultado).

Figuire 11.2: O segundo defeito de Senhora applet.


A listagem 11.2. Mais joaninhas, escaladas.
 1:  import java.awt.Graphics;
 2: import java.awt.Image;
 3:
 4: public class LadyBug2 extends java.applet.Applet {
 5:
 6:     Image bugimg;
 7:
 8:     public void init() {
 9:         bugimg = getImage(getCodeBase(),
10:             "images/ladybug.gif");
11:     }
12:
13:     public void paint(Graphics g) {
14:         int iwidth = bugimg.getWidth(this);
15:         int iheight = bugimg.getHeight(this);
16:         int xpos = 10;
17:
18:         // 25 %
19:        g.drawImage(bugimg, xpos, 10,
20:            iwidth / 4, iheight / 4, this);
21:
22:         // 50 %
23:         xpos += (iwidth / 4) + 10;
24:         g.drawImage(bugimg, xpos , 10,
25:              iwidth / 2, iheight / 2, this);
26:
27:         // 100%
28:         xpos += (iwidth / 2) + 10;
29:         g.drawImage(bugimg, xpos, 10, this);
30:
31:         // 150% x, 25% y
32:         g.drawImage(bugimg, 10, iheight + 30,
33:             (int)(iwidth * 1.5), iheight / 4, this);
34:      }
35: }

Uma nota sobre observadores de imagem

Tenho ignorado firmemente a menção que argumento último a drawImage(): o this misterioso, que também aparece como um argumento a getWidth() e getHeight(). Porque este argumento se usa? O seu uso oficial deve passar em um objeto que funciona como um ImageObserver (isto é, um objeto que implementa a interface de ImageObserver). Os observadores de imagem usam-se para olhar o progresso da que distância ao longo de uma imagem está no processo de carregamento e tomar decisões quando a imagem só se carrega totalmente ou parcialmente. Deste modo, por exemplo, o seu applet pode fazer uma pausa até que todas as imagens se carreguem e prontas, ou exponham uma mensagem "de carregamento" ou façam algo mais enquanto esperava.

A classe de Applet, da qual o seu applet herda, contém um comportamento à revelia da observação de imagem (que herda da superclasse de Component) que deve trabalhar na maioria de casos daqui, o argumento de this a drawImage(), getWidth(), and getHeight(). A única razão quererá usar um argumento alternativo no seu lugar consiste em se quiser mais controle sob o que o seu applet fará em casos onde uma imagem só pode carregar-se parcialmente, ou seguindo a pista de muitas imagens que carregam assincronamente.

Aprenderá mais sobre como tratar com observadores de imagem no Dia 24, "Animação promovida e Meios de comunicação".

Modificação de imagens

Além dos fundamentos de tratar imagens descritas nesta seção, o pacote de java.awt.image fornece mais classes e interfaces que lhe permitem modificar imagens e as suas cores internas, ou criar imagens de mapa de bits à mão. Aprenderá mais sobre a modificação de imagens no Dia 25, "Divertimento com Filtros de Imagem".

Criar animação usando imagens

Criar animação com imagens cria quase o mesmo como a animação com fontes, cores ou formas - usa os mesmos métodos e os mesmos procedimentos para pintura, repintar e redução de bruxuleio sobre que aprendeu ontem. A única diferença é que tem uma pilha de imagens para folhear em vez de grupo de métodos de pintura.

Provavelmente o melhor modo de mostrar-lhe como usar imagens para a animação simplesmente é andar por um exemplo. Aqui está um extenso de uma animação de um pequeno gato chamado Neko.

Um exemplo: Neko

Neko foi uma pequena animação/jogo de Macintosh escrita e desenhada por Kenji Gotoh em 1989. "Neko" é japonês para "o gato", e a animação é de um pequeno gatinho que persegue o ponteiro de rato em volta da tela, sonos, arranhões, e geralmente atua atraente. O programa de Neko transportou-se em diagonal desde então a quase cada plataforma possível, bem como reescreveu-se como um screensaver popular.

Para este exemplo, implementará uma pequena animação baseada na gráfica de Neko original. Diferentemente de Neko original o gato, que foi autônomo (pode "sentir" as bordas da janela e virar e correr em uma direção diferente), este applet simplesmente faz que a Neko corra em do lado abandonado da tela, pare no meio, bocejo, arranhe a sua orelha, durma um pouco, e logo fugir à direita.

Observar
Isto é de muito o maior do applets discutido por enquanto neste livro, e se o imprimir aqui e logo o descrever ou o construir linha pela linha, estará aqui para dias. Em vez disso, vou descrever as partes deste applet independentemente, e vou omitir os fundamentos - o material que aprendeu ontem sobre começo e paragem de fios, o que o método de run() faz, e assim por diante. Todo o código imprime-se depois hoje para que possa juntar tudo isso.

O passo 1: reúna as suas imagens

Antes que comece a escrever que a código de Java construa uma animação, deve ter todas as imagens que formam a própria animação. Para esta versão de Neko há nove deles (o original tem 36), como mostrado na Figura 11.3.

A figura 11.3: As imagens de Neko.

Observar
As imagens de Neko, bem como o texto fonte deste applet, estão disponíveis no CD.

Para este exemplo guardei estas imagens em um diretório chamado, apropriadamente, images. Onde guarda as suas imagens não é tudo de que importante, mas deve tomar nota onde os pôs porque precisará daquela informação mais tarde quando carrega as suas imagens.

O passo 2: organize e carregue as imagens no seu Applet

Agora, no applet. A ideia básica aqui consiste em que tem o grupo de imagens e expõe-nos um após outro, rapidamente, para que deem a aparência do movimento. O modo mais fácil de dirigir isto em Java é guardar as imagens em uma tabela da classe Image, e logo ter uma variável especial para guardar a pista da imagem atual. Como repete sobre as fendas na tabela (usando um laço de for), pode modificar o valor da imagem atual cada vez.

Para Neko applet, criará variáveis de exemplo para implementar ambas estas coisas: uma tabela para manter as imagens, chamadas nekopics e uma variável do tipo Image chamado currentimg, manter a imagem atual que se expõe:

Image nekopics[] = new Image[9];
Image currentimg;

Aqui a tabela de imagem tem nove fendas, como a animação de Neko tem nove imagens. Se tiver um jogo maior ou mais pequeno de imagens, terá um número diferente de fendas.

Nota técnica
A classe de java.util contém uma classe (HashTable) que implementa uma tabela hash. Para grandes números de imagens, uma tabela hash é mais rápida para encontrar e recuperar imagens de do que uma tabela é. Como há um pequeno número de imagens aqui, e porque as tabelas são melhores para o comprimento fixo, repetindo a animação, usarei uma tabela aqui.

Como a animação de Neko desenha as imagens de gato em posições diferentes quanto à tela, também quererá guardar a pista da corrente x e posições y para que vários métodos neste applet saibam onde começar a desenhar. O y fica constante para este determinado applet (Neko corre da esquerda à direita na mesma posição y), mas o x pode variar. Vamos acrescentar duas variáveis de exemplo daquelas duas posições:

int xpos;
int ypos = 50;

Agora, no corpo do applet. Durante a inicialização do applet, lerá em todas as imagens e os guardará na tabela de nekopics. Isto é o tipo da operação que trabalha especialmente bem em um método de init().

Considerando que tem nove imagens com nove nomes de arquivo diferentes, pode fazer uma chamada separada a getImage() para cada um. Pode salvar pelo menos uma pequena datilografia, contudo, criando uma tabela local dos nomes do arquivo (nekosrc, uma tabela de cadeias) e logo usar um laço de for para repetir sobre cada um e carregá-los à sua vez. Aqui está o método de init() de Neko applet que carrega todas as imagens na tabela de nekopics:

public void init() {

    String nekosrc[] = { "right1.gif", "right2.gif",
            "stop.gif", "yawn.gif", "scratch1.gif",
            "scratch2.gif","sleep1.gif", "sleep2.gif",
            "awake.gif" };
    for (int i=0; i < nekopics.length; i++) {
        nekopics[i] = getImage(getCodeBase(),
            "images/" + nekosrc[i]);
    }
}

Observe aqui na chamada a getImage() que o diretório no qual estas imagens se guardam (o diretório de imagem) se inclui como parte do caminho.

O passo 3: anime as imagens

Com as imagens carregadas, o seguinte passo deve começar a animar os bits do applet. Faz este interior o método de run() do fio do applet. Neste applet, Neko faz cinco coisas principais:

Embora possa animar este applet pintando simplesmente a imagem direita à tela no momento oportuno, faz mais sentido escrever este applet para que muitas de atividades de Neko se contenham em métodos individuais. Estes caminho, pode reutilizar algumas atividades (a animação de Neko que corre, especialmente) se quiser que Neko faça coisas em uma ordem diferente.

Vamos começar criando um método para fazer Neko correr. Como vai estar usando este duas vezes, fazendo-o genérico é um bom plano. Vamos criar um método de nekorun(), que toma dois argumentos: a posição de x para começar, e a posição de x para terminar. Neko então corre entre aquelas duas posições (o y permanece constante).

void nekorun(int start, int end) {
...
}

Há duas imagens que representam Neko que corre; para criar o efeito corrente, tem de alternar entre aquelas duas imagens (guardado em posições 0 e 1 da tabela de imagem), bem como movê-los através da tela. A parte móvel é um laço de for simples entre o start e argumentos de end, estabelecendo a posição de x no valor de laço atual. A troca das imagens significa simplesmente testando ver que é ativo em qualquer volta do laço e designação de outra uma à imagem atual. Finalmente, em cada nova armação, chamará repaint() e sleep() para um bocado para fazer uma pausa a animação.

De fato, dado que durante esta animação haverá muita pausa de vários intervalos, faz sentido para criar um método de serviço que faz somente a que-pausa de um período de tempo dado. O método de pause(), por isso, toma um argumento, um número de milissegundos. Aqui está a sua definição:

void pause(int time) {
    try { Thread.sleep(time); }
    catch (InterruptedException e) { }
}

Para trás ao método de nekorun(). Para resumir, nekorun() repete da posição de start à posição de end. Para cada volta do laço, estabelece a posição de x atual, estabelece currentimg na armação de animação direita, chama repaint() e pausas. Adquiriu-o? Aqui está a definição de nekorun:

void nekorun(int start, int end) {
    for (int i = start; i < end; i+=10) {
        xpos = i;
        // swap images
        if (currentimg == nekopics[0])
            currentimg = nekopics[1];
        else currentimg = nekopics[0];
        repaint();
        pause(150);
    }
}

Observe que naquela segunda linha incrementa o laço por 10 pixéis. Porque 10 pixéis e não, dizem, 5 ou 8? A resposta determina-se pela maior parte por meio de prova e erro de ver o que parece direito. Dez parece trabalhar melhor para a animação. Quando escreve a sua própria animação, tem de jogar tanto com as distâncias como com os tempos de sono até que adquira uma animação da qual você gosta.

A fala de repaint(), vamos omitir a que o método de paint(), que pinta cada armação. Aqui o método de paint() é trivialmente simples; todo o paint() é responsável por pinta a imagem atual no x atual e posições de y. Toda aquela informação guarda-se em variáveis de exemplo. Contudo, realmente queremos assegurar-nos que as imagens de fato existem antes que os desenhemos (as imagens poderiam estar no processo do carregamento). Para pegar isto e assegurar-nos não tentamos desenhar uma imagem que não está lá (resultando em todas as espécies de erros), testaremos para assegurar-nos que currentimg não é nulo antes de chamar drawImage() para pintar a imagem:

public void paint(Graphics g) {
    if (currentimg != null)
       g.drawImage(currentimg, xpos, ypos, this);
}

Agora vamos suportar ao método de run(), onde o processamento principal desta animação acontece. Criou o método de nekorun(); em run() chamará aquele método com os valores apropriados para fazer Neko correr da borda esquerda da tela ao centro:

// run from one side of the screen to the middle
nekorun(0, size().width / 2);

A segunda coisa principal que Neko faz nesta animação é a parada e o bocejo. Tem uma armação única de cada uma destas coisas (em posições 2 e 3 na tabela), portanto realmente não precisa de um método separado para desenhá-los. Tudo que tem de fazer estabelece-se a imagem apropriada, chame repaint() e pausa do período de tempo direito. Este exemplo pausas durante um segundo cada vez tanto de paragem como de bocejo novamente, usando prova e erro. Aqui está o código:

// stop and pause
currentimg = nekopics[2];
repaint();
pause(1000);

// yawn
currentimg = nekopics[3];
repaint();
pause(1000);

Vamos mudar à terceira parte da animação: Neko que coça. Não há movimento horizontal para esta parte da animação. Alterna entre duas imagens coçam (guardado em posições 4 e 5 da tabela de imagem). Como o aranhão é uma ação distinta, contudo, vamos criar um método separado para ele.

O método de nekoscratch() toma um argumento único: o número de vezes para coçar. Com aquele argumento, pode repetir, e logo, dentro do laço, alternativo entre duas imagens coçam e repaint cada vez:

void nekoscratch(int numtimes) {
    for (int i = numtimes; i > 0; i--) {
        currentimg = nekopics[4];
        repaint();
        pause(150);
        currentimg = nekopics[5];
        repaint();
        pause(150);
    }
}

Dentro do método de run, então pode chamar nekoscratch() com um argumento de (4):

// scratch four times
nekoscratch(4);

Para a frente! Depois de aranhão, sonos de Neko. Novamente, tem duas imagens para dormir (em posições 6 e 7 da tabela), que alternará um certo número de tempos. Aqui está o método de nekosleep(), que toma um argumento de número único e anima para que muitas "voltas":

void nekosleep(int numtimes) {
    for (int i = numtimes; i > 0; i--) {
        currentimg = nekopics[6];
        repaint();
        pause(250);
        currentimg = nekopics[7];
        repaint();
        pause(250);
    }
}

Chame nekosleep() no método de run() como isto:

// sleep for 5 "turns"
nekosleep(5);

Finalmente, para terminar o applet, Neko desperta-se e foge ao lado direito da tela. A imagem que se desperta é a imagem última na tabela (posição 8), e pode reutilizar o método de nekorun para terminar:

// wake up and run off
currentimg = nekopics[8];
repaint();
pause(500);
nekorun(xpos, size().width + 10);

O passo 4: fim

Há uma mais coisa deixada fazer para terminar o applet. As imagens da animação todos têm contextos brancos. Desenhar aquelas imagens no default applet contexto (um cinzento médio) significa uma caixa branca pouco apresentável em volta de cada imagem. Para vir em volta do problema, simplesmente estabeleça o contexto do applet em white na partida do método de run():

setBackground(Color.white);

Adquiriu tudo isto? Há muito código neste applet e muitos métodos individuais para realizar uma animação bastante simples, mas não é tudo isto complicado. O coração dele, como no coração de todas as formas da animação em Java, deve fundar a armação e logo chamar repaint() para permitir à tela desenhar-se.

Observe que não faz nada para reduzir o montante do bruxuleio neste applet. Resulta que as imagens são bastante pequenas, e a área de desenho também bastante pequena, que o bruxuleio não é um problema deste applet. Sempre é uma boa ideia de escrever que à sua animação faça a coisa mais simples primeiro, e logo acrescentar o comportamento de fazê-lo dirigir o detergente.

Para terminar esta seção, a Listagem 11.3 mostra o código completo de Neko applet.


A listagem 11.3. O final Neko applet.
  1:  import java.awt.Graphics;
  2:  import java.awt.Image;
  3:  import java.awt.Color;
  4:
  5:  public class Neko extends java.applet.Applet
  6:      implements Runnable {
  7:
  8:      Image nekopics[] = new Image[9];
  9:      Image currentimg;
 10:      Thread runner;
 11:      int xpos;
 12:      int ypos = 50;
 13:
 14:      public void init() {
 15:              String nekosrc[] = { "right1.gif", "right2.gif",
 16:              "stop.gif", "yawn.gif", "scratch1.gif",
 17:              "scratch2.gif","sleep1.gif", "sleep2.gif",
 18:              "awake.gif" };
 19:
 20:          for (int i=0; i < nekopics.length; i++) {
 21:              nekopics[i] = getImage(getCodeBase(),
 22:              "images/" + nekosrc[i]);
 23:          }
 24:      }
 25:      public void start() {
 26:          if (runner == null) {
 27:              runner = new Thread(this);
 28:              runner.start();
 29:          }
 30:      }
 31:
 32:      public void stop() {
 33:          if (runner != null) {
 34:              runner.stop();
 35:              runner = null;
 36:          }
 37:      }
 38:
 39:      public void run() {
 40:
 41:          setBackground(Color.white);
 42:
 43:          // run from one side of the screen to the middle
 44:          nekorun(0, size().width / 2);
 45:
 46:          // stop and pause
 47:          currentimg = nekopics[2];
 48:          repaint();
 49:          pause(1000);
 50:
 51:          // yawn
 52:          currentimg = nekopics[3];
 53:          repaint();
 54:          pause(1000);
 55:
 56:          // scratch four times
 57:          nekoscratch(4);
 58:
 59:          // sleep for 5 "turns"
 60:          nekosleep(5);
 61:
 62:          // wake up and run off
 63:          currentimg = nekopics[8];
 64:          repaint();
 65:          pause(500);
 66:          nekorun(xpos, size().width + 10);
 67:      }
 68:
 69:      void nekorun(int start, int end) {
 70:          for (int i = start; i < end; i += 10) {
 71:              xpos = i;
 72:              // swap images
 73:              if (currentimg == nekopics[0])
 74:                 currentimg = nekopics[1];
 75:              else currentimg = nekopics[0];
 76:              repaint();
 77:              pause(150);
 78:          }
 79:      }
 80:
 81:      void nekoscratch(int numtimes) {
 82:         for (int i = numtimes; i > 0; i--) {
 83:              currentimg = nekopics[4];
 84:              repaint();
 85:              pause(150);
 86:              currentimg = nekopics[5];
 87:              repaint();
 88:              pause(150);
 89:          }
 90:      }
 91:
 92:      void nekosleep(int numtimes) {
 93:         for (int i = numtimes; i > 0; i--) {
 94:              currentimg = nekopics[6];
 95:              repaint();
 96:              pause(250);
 97:              currentimg = nekopics[7];
 98:              repaint();
 99:              pause(250);
100:          }
101:
102:      void pause(int time) {
103:         try { Thread.sleep(time); }
104:          catch (InterruptedException e) { }
105:      }
106:
107:      public void paint(Graphics g) {
108:         if (currentimg != null)
109:           g.drawImage(currentimg, xpos, ypos, this);
110:      }
111: }

Recuperar e utilização de sons

Java tem o suporte construído de jogar sons em conjunto com a animação corrente ou para sons sozinhos. De fato, o suporte do som, como suporte de imagens, incorpora-se no Applet e classes de awt, usando então soe no seu Java applets é tão fácil como imagens carregam e usam.

Atualmente, o único formato sólido que Java apoia é o formato de AU de Sol, m às vezes chamado de formato legal. Os arquivos de AU tendem a ser mais pequenos do que arquivos sólidos em outros formatos, mas a qualidade sólida não é muito boa. Se se preocupar especialmente com a qualidade sólida, pode querer que os seus clipes sólidos sejam referências no HTML tradicional caminho (como conexões a arquivos externos) em vez de incluído em Java applet.

O modo mais simples de recuperar e jogar um som é pelo método de play(), parte da classe de Applet e por isso disponível para você no seu applets. O método de play() é semelhante ao método de getImage() em que toma uma de duas formas:

Por exemplo, a seguinte linha do código recupera e joga o meow.au sólido, que se contém no diretório áudio. O diretório áudio, à sua vez, localiza-se no mesmo diretório que este applet:

play(getCodeBase(), "audio/meow.au");

O método de play() recupera e joga o som dado o mais logo possível depois que se chama. Se não puder encontrar o som, não adquirirá um erro; somente não adquirirá nenhum áudio quando o esperar.

Se quiser jogar um som repetidamente, comece e pare o clipe sólido ou corra o clipe como um laço (jogue-o repetidas vezes), as coisas são ligeiramente mais complicadas - mas não muito mais. Neste caso, usa o método applet getAudioClip() para carregar o clipe sólido em um exemplo da classe AudioClip (parte de java.applet - não esquecem de importá-lo) e logo produza diretamente aquele objeto de AudioClip.

Suponha, por exemplo, que tem um laço sólido que quer jogar em background do seu applet. No seu código de inicialização, pode usar esta linha para adquirir o clipe áudio:

AudioClip clip = getAudioClip(getCodeBase(),
    "audio/loop.au");

Então, para jogar o clipe uma vez, use o método de play():

clip.play();

Para parar um atualmente clipe de som de jogo, use o método de stop():

clip.stop();

Para firmar o clipe com laço (o jogam repetidamente), use o método de loop():

clip.loop();

Se o método de getAudioClip() não puder encontrar o som indica ou não pode carregá-lo por nenhuma razão, devolve null. É uma boa ideia de testar para este caso no seu código antes de tentar jogar o clipe áudio, porque tentando chamar o play(), stop() e os métodos de loop() em um objeto de null resultarão em um erro (de fato, uma exceção).

No seu applet, pode jogar tantos clipes áudio como precisa; todos os sons que usa vão se misturar em conjunto propriamente como se jogam pelo seu applet.

Observe que se usar um clipe sólido sólido-a de fundo que os laços repetidamente - que o clipe sólido não deixará de jogar automaticamente quando suspende o fio do applet. Isto significa que mesmo se o seu leitor se mover para outra página, os sons do primeiro applet continuarão jogando. Pode fixar este problema parando o som de fundo do applet no seu método de stop():

public void stop() {
    if (runner != null) {
        if (bgsound != null)
            bgsound.stop();
        runner.stop();
        runner = null;
    }
}

A listagem 11.4 mostra uma armação simples de um applet que joga dois sons: O primeiro, um som de fundo chamado loop.au, joga repetidamente. O segundo, um chifre que buzina (beep.au), joga cada 5 segundos. (Não me preocuparei com dar-lhe um quadro deste applet porque não expõe de fato nada outro do que uma cadeia simples à tela.)


A listagem 11.4. O AudioLoop applet.
 1: import java.awt.Graphics;
 2: import java.applet.AudioClip;
 3:
 4: public class AudioLoop extends java.applet.Applet
 5:  implements Runnable {
 6:
 7:     AudioClip bgsound;
 8:     AudioClip beep;
 9:     Thread runner;
10:
11:     public void start() {
12:         if (runner == null) {
13:             runner = new Thread(this);
14:             runner.start();
15:          }
16:      }
17:
18:     public void stop() {
19:         if (runner != null) {
20:             if (bgsound != null) bgsound.stop();
21:             runner.stop();
22:             runner = null;
23:         }
24:     }
25:
26:     public void init() {
27:         bgsound = getAudioClip(getCodeBase(),"audio/loop.au");
28:         beep = getAudioClip(getCodeBase(), "audio/beep.au");
29:     }
30:
31:     public void run() {
32:         if (bgsound != null) bgsound.loop();
33:         while (runner != null) {
34:             try { Thread.sleep(5000); }
35:             catch (InterruptedException e) { }
36:             if (beep != null) beep.play();
37:         }
38:     }
39:
40:     public void paint(Graphics g) {
41:         g.drawString("Playing Sounds....", 10, 10);
42:     }
43: }

Há só algumas coisas a observar sobre este applet. Em primeiro lugar, observe o método de init() em linhas 26 para 29, que carrega tanto o loop.au como os arquivos de som de beep.au. Não fizemos nenhuma tentativa aqui para assegurar-nos que estes arquivos de fato carregam como esperado, portanto a possibilidade existe que o bgsound e as variáveis de exemplo de beep podem terminar com os valores nulos se o arquivo não puder carregar. Neste caso, não seremos capazes de chamar loop(), stop() ou qualquer outro método, portanto devemos assegurar-nos que testamos para isto em outro lugar no applet.

E testamos para vários lugares nulos aqui, em particular no método de run() em linhas 32 e 36. Estas linhas começam a rotação de sons e jogo, mas só se os valores do bgsound e variáveis de beep são outra coisa do que nulo.

Finalmente, observe a linha 20, que explicitamente apaga o som de fundo se o fio também se estiver parando. Como os sons de fundo não deixam de jogar mesmo quando o fio se parou, tem de pará-los explicitamente aqui.

Utilização de pacotes de animação

Até este ponto, descrevi a animação em um montante justo do detalhe, para ajudar a explicar outros tópicos que pode usar em applets que são não necessariamente animação (por exemplo, gráfica, fios, imagens de mapa de bits gerentes).

Se o objetivo do seu applet for animação, contudo, em muitos casos escrevendo que o seu próprio applet é massacre. Existem applets de uso geral que fazem apenas a animação, e pode usar aqueles applets nas suas próprias Páginas da Web com o seu próprio jogo de imagens - tudo que tem de fazer é modificam os arquivos de HTML para dar parâmetros diferentes ao próprio applet. A utilização destes pacotes faz a animação simples que cria em Java muito mais fácil, em particular para desenvolvedores de Java que não estão como bons no lado de programação de Java.

Dois pacotes de animação são especialmente úteis neste aspecto: Animator applet de Sol e Dimensão o Movimento Líquido de X.

Animator Applet de sol

Animator applet de sol, um dos exemplos nos 1.0.2 JDK, fornece um applet simples, de uso geral para criar a animação com Java. Compila o código e cria um arquivo de HTML com os parâmetros apropriados da animação. Usando Animator applet, pode fazer o seguinte:

Mesmo se não pretende usar Animator de Sol para a sua própria animação, poderia querer olhar para o código. Animator applet é um grande exemplo de como a animação trabalha em Java e os tipos de truques inteligentes que pode usar em Java applet.

Dimensão o movimento líquido de X

Enquanto Animator applet de Sol é um simples (e livre) o exemplo de um instrumento de animação de uso geral, o Movimento Líquido da Dimensão X é muito mais ambicioso. O Movimento líquido é uma aplicação GUI inteira, que corre em Java, com o qual constrói a animação (chamam-nos cenas) dado grupo de arquivos de mídia (imagens e som). Se tenha usado alguma vez o Diretor de Macromeios de comunicação para criar apresentações de multimédia (ou apresentações de Shockwave da Web), é familiar com a aproximação. Para usar o Movimento Líquido, importa os seus arquivos de mídia, e logo pode arranjar imagens na tela, arranjá-los em armações sobre pontos a tempo, fazê-los avançar caminhos predestinados e acrescentar cores e contextos e trilhas de áudio simplesmente clicando em botões. A figura 11.4 mostra a tela Liquid Motion principal.

A figura 11.4: movimento líquido.

Quando salva uma cena de Movimento Líquida como HTML, o programa salva todos os arquivos de classe de Java precisará de dirigir a apresentação e escreve um arquivo de HTML, completo das etiquetas de <APPLET> apropriadas e parâmetros, para dirigir aquela cena. Tudo que tem de fazer é mover os arquivos para o seu Servidor Web e faz-se-there's nenhuma programação de Java implicou que. Mas mesmo se é um programador de Java (como será em que termina este livro), pode estender a armação de Movimento Líquida para incluir novo comportamento e características.

Como o Movimento Líquido é uma aplicação de Java, corre em qualquer plataforma que Java continua (Windows, UNIX, Mac). É uma aplicação comercial, US$ 149,99 de valor do Windows e versões UNIX (a versão de Mac existe, mas não parece custar algo). As cópias de manifestação de Solaris e versões de Windows, que lhe permitem jogar com a interface mas não publicar os arquivos na Web, estão disponíveis no Web site de X de Dimensão.

Vale a pena verificar o Movimento líquido se pretender fazer muito tipo da animação applets nas suas Páginas da Web; usar Movimento Líquido o seu regularmente fácil para levantar-se e gerência, muito mais rápido do que o trabalho diretamente com o código. Verifique http://www.dimensionx.com/products/lm/ de mais informação e versões de manifestação.

Mais sobre bruxuleio: armazenar em buffer duplamente

Ontem aprendeu dois modos simples de reduzir piscada na animação de Java. Embora aprendesse especificamente sobre o desenho de utilização de animação, o bruxuleio também pode resultar da animação usando imagens. Além de dois métodos reduzem o bruxuleio descritos ontem, há um outro modo de reduzir o bruxuleio: armazenar em buffer duplamente.

Com o armazenar em buffer duplamente, cria uma segunda superfície (fora da tela, assim para falar), faça toda a sua pintura àquele fora da tela superfície, e logo desenhe a superfície inteira ao mesmo tempo para o applet real (e para a tela) no fim - em vez de desenhar para a superfície de gráficos real do applet. Como todo o trabalho de fato continua nos bastidores, não há oportunidade para partes provisórias do processo de desenho para aparecer acidentalmente e interromper a lisura da animação.

O armazenar em buffer duplamente é o processo de fazer todo o seu desenho a um fora da tela buffer e logo exposição que tela inteira ao mesmo tempo. Chamou o armazenar em buffer duplamente porque há dois buffers de desenho e liga entre eles.

O armazenar em buffer duplamente é não sempre a melhor solução. Se o seu applet estiver sofrendo do bruxuleio, tente ignorar update() e só desenhar porções da tela primeiro; isto pode resolver o seu problema. O armazenar em buffer duplamente é menos eficiente do que regular armazenar em buffer e também toma mais memória e espaço, portanto, se puder evitá-lo, fazem um esforço de fazer assim. Quanto a eliminar quase bruxuleio de animação, contudo, o armazenar em buffer duplamente trabalha excepcionalmente bem.

Criar Applets com armazenar em buffer duplamente

Para criar um applet que usa o armazenar em buffer duplamente, precisa de duas coisas: fora da tela imagem para aproximar-se e um contexto de gráficos daquela imagem. Aqueles dois em conjunto imitam o efeito do applet's que desenha superfície: o contexto de gráficos (um exemplo de Graphics) para fornecer os métodos de desenho, como drawImage (e drawString), e o Image para manter os pontos que se desenham.

Há quatro passos principais à soma de armazenar em buffer duplamente ao seu applet. Em primeiro lugar, o seu fora da tela a imagem e o contexto de gráficos têm de guardar-se em variáveis de exemplo para que possa passá-los ao método de paint(). Declare as seguintes variáveis de exemplo na sua definição de classe:

Image offscreenImage;
Graphics offscreenGraphics;

Em segundo lugar, durante a inicialização do applet, criará um Image e um Graphics objetam e destinam-nos a estas variáveis (tem de esperar até a inicialização portanto sabe de que tamanho vão ser). O método de createImage() dá-lhe um exemplo de Image, que então pode enviar ao método de getGraphics() para adquirir um novo contexto de gráficos daquela imagem:

offscreenImage = createImage(size().width,
    size().height);
offscreenGraphics = offscreenImage.getGraphics();

Agora, sempre que tenha de desenhar à tela (normalmente no seu método de paint()), em vez de desenhar à gráfica de paint, desenhar ao fora da tela gráfica. Por exemplo, para desenhar uma imagem chamada img na posição 10,10, use esta linha:

offscreenGraphics.drawImage(img, 10, 10, this);

Finalmente, no fim do seu método de paint, depois de todo o desenho a fora da tela a imagem faz-se, acrescente a seguinte linha para colocar o fora da tela buffer na verdadeira tela:

g.drawImage(offscreenImage, 0, 0, this);

Naturalmente, mais provavelmente quererá ignorar update() para que não compense a tela entre pinturas:

public void update(Graphics g) {
    paint(g);
}

Vamos rever aqueles quatro passos:

  1. Acrescente variáveis de exemplo para manter a imagem e contextos de gráficos para o fora da tela buffer.
  2. Crie uma imagem e um contexto de gráficos quando o seu applet se inicializa.
  3. Faça todo o seu applet que pinta ao fora da tela buffer, não o applet's que desenha superfície.
  4. No fim do seu método de paint(), desenhe o fora da tela buffer à verdadeira tela.

Uma nota de disposição de contextos de gráficos

Se fizer o uso extensivo de contextos de gráficos no seu applets ou aplicações, sabem que aqueles contextos muitas vezes continuarão ficando em volta depois que se faz com eles, mesmo se já não tiver referências para eles. Os contextos de gráficos são objetos especiais nos awt que fazem o mapa ao sistema operacional nativo; o coletor de lixo de Java não pode lançar aqueles contextos por si mesmo. Se usar múltiplos contextos de gráficos ou os usar repetidamente, quererá livrar-se explicitamente daqueles contextos uma vez que se faz com eles.

Use o método de dispose() para limpar explicitamente um contexto de gráficos. Um bom lugar de pôr isto poderia estar no método de destroy() do applet (sobre o qual aprendeu no Dia 8, "Java Fundamentos de Applet"; foi um dos métodos applet primários, junto com init(), start() e stop()):

public void destroy() {
  offscreenGraphics.dispose();
}

Um exemplo: damas revisitadas

O exemplo de ontem apresentou o oval vermelho móvel animado para demonstrar o bruxuleio de animação e como reduzi-lo. Mesmo com as operações fez ontem, contudo, as Damas applet ainda aceso ocasionalmente. Vamos revisar isto applet para incluir o armazenar em buffer duplamente.

Em primeiro lugar, acrescente as variáveis de exemplo para o fora da tela imagem e o seu contexto de gráficos:

Image offscreenImg;
Graphics offscreenG;

Em segundo lugar, acrescente um método de init para inicializar o fora da tela buffer:

public void init() {
    offscreenImg = createImage(size().width, size().height);
    offscreenG = offscreenImg.getGraphics();
}

Em terceiro lugar, modifique o método de paint() para desenhar ao fora da tela buffer em vez de ao buffer de gráficos principal:

public void paint(Graphics g) {
    // Draw background
    offscreenG.setColor(Color.black);
    offscreenG.fillRect(0, 0, 100, 100);
    offscreenG.setColor(Color.white);
    offscreenG.fillRect(100, 0, 100, 100);

    // Draw checker
    offscreenG.setColor(Color.red);
    offscreenG.fillOval(xpos, 5, 90, 90);

    g.drawImage(offscreenImg, 0, 0, this);
}

Observe que ainda tosquia o retângulo de gráficos principal no método de update(), como fez ontem; não tem de modificar aquela parte. A única parte que é relevante é que a linha final no método de paint() em que tudo se desenha fora da tela antes finalmente expor-se.

Finalmente, no método de destroy() do applet vamos nos desfazer explicitamente do contexto de gráficos guardado em offscreenG:

public void destroy() {
   offscreenG.dispose();
}

A listagem 11.5 mostra o código final das Damas applet (Checkers3.java), que inclui o armazenar em buffer duplamente.


A listagem 11.5. Damas revisitadas, com armazenar em buffer duplamente.
 1: import java.awt.Graphics;
 2: import java.awt.Color;
 3: import java.awt.Image;
 4:
 5: public class Checkers3 extends java.applet.Applet implements Runnable {
 6:
 7:   Thread runner;
 8:   int xpos;
 9:   int ux1,ux2;
10:   Image offscreenImg;
11:   Graphics offscreenG;
12:
13:   public void init() {
14:     offscreenImg = createImage(this.size().width, this.size().height);
15:     offscreenG = offscreenImg.getGraphics();
16:   }
17:
18:   public void start() {
19:     if (runner == null); {
20:       runner = new Thread(this);
21:       runner.start();
22:     }
23:   }
24:
25:   public void stop() {
26:     if (runner != null) {
27:       runner.stop();
28:       runner = null;
29:     }
30:   }
31:
32:   public void run() {
33:     setBackground(Color.blue);
34:     while (true) {
35:       for (xpos = 5; xpos <= 105; xpos+=4) {
36:          if (xpos == 5) ux2 = size().width;
37:          else ux2 = xpos + 90;
38:          repaint();
39:          try { Thread.sleep(100); }
40:          catch (InterruptedException e) { }
41:          if (ux1 == 0) ux1 = xpos;
42:       }
43:       xpos = 5;
44:     }
45:   }
46:
47:   public void update(Graphics g) {
48:     g.clipRect(ux1, 5, ux2 - ux1, 95);
49:     paint(g);
50:   }
51:
52:   public void paint(Graphics g) {
53:     // Draw background
54:     offscreenG.setColor(Color.black);
55:     offscreenG.fillRect(0,0,100,100);
56:     offscreenG.setColor(Color.white);
57:     offscreenG.fillRect(100,0,100,100);
58:
59:     // Draw checker
60:     offscreenG.setColor(Color.red);
61:     offscreenG.fillOval(xpos,5,90,90);
62:
63:     g.drawImage(offscreenImg,0,0,this);
64:
65:     // reset the drawing area
66:     ux1 = ux2 = 0;
67:   }
68:
69:   public void destroy() {
70:      offscreenG.dispose();
71:   }
72: }

Sumário

Três tópicos principais são o foco da lição de hoje. Em primeiro lugar, aprendeu sobre a utilização de imagens na sua applets-localização deles, carregamento deles e utilização do método de drawImage() para expor-os, no seu tamanho normal ou escalado a tamanhos diferentes. Também aprendeu como criar a animação em Java usando imagens.

Em segundo lugar, aprendeu como usar sons, que podem incluir-se no seu applets qualquer tempo precisa deles - em momentos específicos ou como o contexto soa o que pode repetir-se enquanto o applet realiza. Aprendeu como localizar, carregar, e jogar sons usando tanto o play() como os métodos de getAudioClip().

Finalmente, aprendeu sobre o armazenar em buffer duplamente, uma técnica que lhe permite eliminar praticamente o bruxuleio na sua animação, por alguma despesa de eficiência de animação e velocidade. Usando imagens e contextos de gráficos, pode criar um fora da tela buffer para desenhar a, o resultado do qual então se expõe à tela no último momento possível.

Perguntas e Respostas

Q:
No programa de Neko, põe o carregamento de imagem no método de init(). Parece-me que poderia levar muito tempo a Java para carregar todas aquelas imagens, e porque init() não está no fio principal do applet, vai haver uma pausa distinta lá. Não porque pôr o carregamento de imagem no início do método de run() em vez disso?
A:
Há coisas furtivas que continuam nos bastidores. O método de getImage() não carrega de fato a imagem; de fato, devolve um objeto de Image quase instantaneamente, portanto não toma um grande montante do tempo de processamento durante a inicialização. Os dados de imagem para que getImage() aponta não se carregam de fato até que a imagem seja necessária. Estes caminho, Java não tem de guardar imagens enormes em volta na memória se o programa estiver indo usar só uma pequena parte. Em vez disso, somente pode guardar uma referência para aqueles dados e recuperar de que precisa depois.
Q:
Compilei e dirigi Neko applet. Algo esquisito continua; a animação começa em armações de baixas e o meio. É como se só algumas imagens tenham carregado quando o applet se dirige.
A:
Isto é precisamente o que está acontecendo. Como o carregamento de imagem não carrega de fato a imagem imediatamente, o seu applet pode estar animando alegremente telas em branco enquanto as imagens ainda se estão carregando. Dependendo de quanto tempo toma aquelas imagens para carregar, o seu applet pode parecer começar no meio, deixar armações ou não trabalhar em absoluto.
Há três soluções possíveis para este problema. O primeiro deve ter o laço de animação (isto é, comece do começo uma vez que para). Consequentemente as imagens carregarão e a animação trabalhará corretamente. A segunda solução, e não um muito bom, é dormir durante algum tempo antes de começar a animação, para fazer uma pausa enquanto as imagens carregam. A terceira, e melhor solução, deve usar observadores de imagem não para se assegurar nenhuma parte dos jogos de animação antes que as suas imagens tenham carregado. Aprenderá mais sobre observadores de imagem no Dia 24.
Q:
Escrevi que a um applet fizesse um som de fundo usando métodos de loop() e o getAudioClip(). Os trabalhos sólidos grandes, mas não parará. Tentei suspender o fluxo corrente e matar o fio em conjunto, mas o som continua.
A:
Mencionei isto como uma pequena nota na seção em sons; os sons de fundo não correm no fio principal do applet, portanto se parar o fio, o som continua indo. A solução é fácil - no mesmo método onde para o fio, também pare o som, como isto:

os runner.stop ()//param o fio
os bgsound.stop ()//também param o som

Q:
Se usar o armazenar em buffer duplamente, ainda tenho de tosquiar a uma pequena região da tela? Como o armazenar em buffer duplamente elimina o bruxuleio, parece mais fácil desenhar a armação inteira cada vez.
A:
Mais fácil, sim, mas menos eficiente. O desenho só se separa da tela não só reduz o bruxuleio, muitas vezes também limita o volume de trabalho que o seu applet tem de fazer no método de paint(). Mais rápido os trabalhos de método de paint(), mais rápido e mais liso a sua animação correrá. Usar regiões de clipe e desenhar só o que é necessário são uma boa prática para seguir no general - não somente se tiver um problema com o bruxuleio.