Michael Morrison
Muitas pessoas mexeram-se quando a Web primeiro trouxe imagens coloridas à Internet. Nestes dias, as imagens a cores simplesmente devem esperar-se, enquanto um interesse crescente se está colocando na animação ou imagens móveis. Se um quadro puder dizer mil palavras, imaginar o que um ramo de quadros mostrados muito rapidamente pode dizer!
A lição de hoje concentra-se como o efeito do movimento animado se transmite em Java usando uma série de imagens expostas rapidamente. Esta técnica não é realmente nada de novo a computadores ou programação, embora seja bastante novo para a Web. Se estiver pensando que esta descrição da lição de hoje parece terrivelmente familiar, é porque já aprendeu sobre a animação em lições mais adiantadas. A diferença é que a lição de hoje vai tomá-lo muito além disso na aprendizagem sobre o que a animação é e como fazer algumas coisas realmente potentes com ele.
Mais especificamente, hoje aprenderá sobre o seguinte:
Embora a parte da lição de hoje seja teórica, terminará a lição criando um jogo potente de classes de animação de duende reutilizáveis. Não se incomode se não sabe o que um duende ainda é - vai bastante logo!
Antes de entrar em animação como se relaciona a Java, é importante entender os fundamentos do que a animação é e como trabalha. Por isso vamos começar fazendo a pergunta fundamental: O que é animação? Posto simplesmente, a animação é a ilusão do movimento. Digo-lhe que cada animação que viu alguma vez é realmente somente uma ilusão? Isto é exatamente direito! E provavelmente a ilusão animada mais surpreendente é aquela que capturou a nossa atenção muito antes de computadores modernos - a televisão. Quando assiste televisão, vê muitas coisas que se deslocam, mas o que percebe como o movimento é realmente somente um truque que se joga nos seus olhos.
Novo termo |
A animação é o processo de simular o movimento. |
Em caso da televisão, a ilusão do movimento cria-se expondo uma sucessão rápida de imagens com modificações leves no conteúdo. O olho humano percebe estas modificações como movimento por causa da sua acuidade visual baixa. O dispensarei a lição de biologia de porque isto é assim; o caso é que os nossos olhos são regularmente fáceis enganar em enamorar-se da ilusão da animação. Mais especificamente, o olho humano pode enganar-se no percepção de movimento animado com apenas 12 armações do movimento por segundo. A velocidade de animação mede-se em armações por segundo (fps), que é o número de armações de animação ou modificações de imagem, apresentadas cada segundo.
Novo termo |
As armações por segundo (fps) são o número de armações de animação ou modificações de imagem, apresentadas cada segundo. |
Embora 12fps seja tecnicamente bastante para enganar os nossos olhos na vista de animação, animações em velocidades isto baixo muitas vezes termina de parecer um tanto aos arrancos. A maior parte de animações profissionais, por isso, usam uma tarifa de armação mais alta. A televisão, por exemplo, usa 30fps. Quando vai aos filmes, vê filmes cinematográficos em aproximadamente 24fps. É bastante evidente que estas tarifas de armação são mais do que bastante para cativar a nossa atenção e com sucesso criar a ilusão do movimento.
Programando a animação em Java, tipicamente tem a capacidade de manipular a tarifa de armação um montante decente. A limitação mais óbvia da tarifa de armação é a velocidade na qual o computador pode gerar e expor as armações de animação. Em Java, isto é um ponto crucial porque não se conhece tipicamente que Java applets é demônios de velocidade. Contudo, o lançamento recente de compiladores de Java just-in-time ajudou a acelerar Java applets, junto com o alívio de alguns assuntos de realização associados com a animação.
Observar |
Atualmente, tanto o Navegador de Netscape 3.0 como Microsoft Internet Explorer 3.0 apoiam a compilação just-in-time de Java applets. |
Sei que coça provavelmente para ver alguma verdadeira animação em Java, mas há muitas outras questões para cobrir antes de entrar nos detalhes da programação de animação. Mais especificamente, é importante para você entender que os tipos primários da animação usaram na programação de Java. Há de fato muitos tipos diferentes da animação, todos dos quais são úteis em exemplos diferentes. Contudo, com os objetivos de implementar a animação em Java, rompi a animação em dois tipos básicos: animação baseada na armação e animação baseada na forma.
O tipo mais simples da animação é a animação baseada na armação, que é o tipo primário da animação encontrada na Web. A animação baseada na armação implica o movimento de simulação expondo uma sequência de imagens de armação pregeradas, estáticas. Um filme é um exemplo perfeito da animação baseada na armação; cada armação do filme é uma armação da animação, e quando as armações se mostram um após outro, criam a ilusão do movimento.
Novo termo |
A animação baseada na armação simula o movimento expondo uma sequência de imagens de armação pregeradas, estáticas. |
A animação baseada na armação não tem conceito de um objeto gráfico distinguível do contexto; tudo que aparece em uma armação é parte daquela armação no conjunto. O resultado consiste em que cada imagem de armação contém toda a informação necessária para aquela armação em uma forma estática. Isto é um ponto importante porque distingue a animação baseada na armação da animação baseada na forma, sobre a qual aprenderá depois.
Observar |
A maior parte da animação usada em Web sites implementa-se usando animou imagens GIF, que implica múltiplas armações de animação guardam em um arquivo de imagem GIF único. GIFs animados são um exemplo muito bom da animação baseada na armação. |
Uma técnica de animação mais potente muitas vezes empregada em jogos e software educativo é a animação baseada na forma, que também se conhece como animação de duende. A animação baseada na forma implica objetos gráficos que se movem independentemente de um contexto. Neste ponto, pode confundir-se um pouco pelo meu uso do termo "gráfico de objeto" referindo-se a partes de uma animação. Neste caso, um objeto gráfico é algo em que logicamente podem pensar como uma entidade separada do contexto de uma imagem de animação. Por exemplo, em uma animação do sistema solar, os planetas seriam objetos gráficos separados que são logicamente independentes do contexto estrelado.
Novo termo |
A animação baseada na forma simula o movimento usando objetos gráficos que se movem independentemente de um contexto. |
Cada objeto gráfico em uma animação baseada na forma menciona-se como um duende e pode ter uma posição que varia dentro de algum tempo. Em outras palavras, os duendes mandam associar uma velocidade com eles que determina como a sua posição se modifica dentro de algum tempo. Quase cada jogo de computador usa duendes até certo ponto. Por exemplo, cada objeto no jogo de Asteróides clássico é um duende que se move independentemente do contexto preto.
Novo termo |
Um duende é um objeto gráfico que pode mover-se independentemente de um contexto ou outros objetos. |
Observar |
Pode estar admirando-se onde o termo animação baseada na forma vem de. Vem do fato que podem pensar em duendes como membros de forma que se deslocam em uma etapa. Esta analogia da animação de computador relacionada à realização teatral é muito útil. Pensando em duendes como membros de forma e o contexto como uma etapa, pode dar o seguinte passo lógico e pensar em uma animação como uma realização teatral. De fato, isto não é distante da marca, porque a meta de realizações teatrais é entreter o público contando uma história pela interação dos membros de forma. De mesmo modo, as animações baseadas na forma usam a interação de duendes para entreter o usuário, contando muitas vezes uma história ou pelo menos adquirindo algum ponto através. |
Embora o princípio fundamental atrás da animação de duende seja o movimento posicional de um objeto gráfico, não há razão não pode incorporar a animação baseada na armação em um duende. A incorporação de animação baseada na armação em um duende permite-lhe modificar a imagem do duende bem como alterar a sua posição. Este tipo híbrido da animação é o que implementará depois hoje nas classes de duende de Java.
Mencionei na discussão de animação baseada na armação que a televisão é um bom exemplo da animação baseada na armação. Mas pode pensar em algo pela televisão que se cria em uma maneira semelhante à animação baseada na forma (outro do que filmes animados e caricaturas)? Admirou-se alguma vez como weatherpeople magicamente aparecem em frente de um mapa gerado pelo computador mostrando o tempo? A estação de notícias usa uma técnica conhecida como proteção azul, que lhes permite recobrir o weatherperson em cima do mapa de tempo em tempo real. Trabalha como isto: A pessoa está em frente de um fundo azul, que serve de um contexto transparente. A imagem do weatherperson recobre-se para o mapa de tempo; o truque é que o contexto azul se filtra quando a imagem se recobre para que seja efetivamente transparente. Deste modo, o weatherperson atua exatamente como um duende!
O exemplo weatherperson sobe um ponto muito importante quanto a duendes: transparência. Como as imagens bitmapped são retangulares pela natureza, um problema surge quando as imagens de duende não são retangulares na forma. Em duendes que não são retangulares na forma, que é a maioria de duendes, os pixéis que rodeiam a imagem de duende são não usados. Em um sistema de gráficos sem transparência, estes pixéis não usados desenham-se como qualquer outro. O resultado de fim é duendes que têm bordas retangulares visíveis em volta deles, que completamente destrói a eficácia de mandar recobrir duendes em uma imagem de fundo.
Qual é a solução? Bem, uma solução é fazer todos os seus duendes retangulares. A menos que esteja planejando escrever um applet que mostra dança de caixas, uma solução mais realista é transparência, que lhe permite definir certa cor em uma imagem como não usado, ou transparente. Quando os pixéis desta cor se encontram por rotinas de desenho de gráfica, simplesmente omitem-se, deixando o contexto original intato. As cores transparentes em imagens atuam exatamente como a tela azul da morte do weatherperson.
Novo termo |
As cores transparentes são cores em uma imagem que são não usadas, significando que não se desenham quando o resto das cores na imagem se desenha. |
Está pensando provavelmente que a implementação de transparência implica muito bit de baixo nível brincar e manipulação de pixel de imagem. Em alguns ambientes de programação seria correto nesta suposição, mas não em Java. Afortunadamente, a transparência já se apoia em Java por meio do GIF 89a formato de imagem. No GIF 89a formato de imagem, simplesmente especifica uma cor da imagem GIF que serve da cor transparente. Quando a imagem se desenha, os pixéis que combinam com a cor transparente omitem-se e deixam-se não utilizados, deixando os pixéis de fundo inalterados. Nenhuma mais caixa de dança!
Em muitos exemplos, quererá que alguns duendes apareçam em cima de outros. Por exemplo, na animação de sistema solar quereria ser capaz de ver alguns planetas passar em frente de outros. Trata este problema destinando cada duende de planeta uma profundidade de tela, que é também referida como Z-ordem.
Novo termo |
A Z-ordem é a profundidade relativa de duendes na tela. |
A profundidade de duendes chama-se a Z-ordem porque trabalha o tipo do parecido o outro parecido a uma dimensão um Eixo Z. Pode pensar em duendes que se deslocam na tela no avião XY. Semelhantemente podem pensar no Eixo Z como outro eixo projetado na tela que determina como os duendes ficam sobrepostos um a outro. Para pô-lo outro caminho, a Z-ordem determina a profundidade de um duende dentro da tela. Utilizando um Eixo Z, poderia pensar que os duendes Z-ordered são 3D. A verdade é que os duendes Z-ordered não são 3D porque o Eixo Z é um eixo hipotético que só se usa para determinar como os objetos de duende escondem um a outro. Um verdadeiro duende 3D seria capaz de mover-se tão livremente para o Eixo Z como faz no avião XY.
Somente para assegurar-se que adquire um quadro claro de como a Z-ordem trabalha, vamos atrás por um momento aos bons velhos dias da animação tradicional. animators tradicional, como aqueles em Disney, usou folhas de celulóide para desenhar objetos animados. Atraíram estes porque podem recobrir-se em uma imagem de fundo e mover-se independentemente. Isto conhecia-se como animação de telefone celular e deve parecer vagamente familiar. (A animação de telefone celular é uma primeira versão da animação de duende.) Cada folha de telefone celular corresponde a um valor de Z-ordem único, determinado por onde na pilha de folhas a folha se localiza. Se uma imagem perto do topo da pilha resultar estar na mesma posição na folha de telefone celular como alguma imagem mais baixa, esconde-os. A posição de cada imagem na pilha de folhas de telefone celular é a sua Z-ordem, que determina a sua precedência de visibilidade. A mesma coisa aplica a duendes em animações baseadas na forma, exceto que a Z-ordem se determina pela ordem na qual os duendes se desenham, em vez da posição de folha de telefone celular. Este conceito de uma pilha de folhas de telefone celular que representam todos os duendes em um sistema de duende será útil depois hoje quando desenvolver as classes de duende.
Embora a detecção de choque só seja principalmente útil em jogos, é um componente importante da animação de duende. A detecção de choque é o processo da determinação se os duendes colidiram um com outro. Embora a detecção de choque não desempenhe diretamente um papel na criação da ilusão do movimento, liga-se justamente à animação de duende e extremamente útil em alguns cenários, como jogos.
Novo termo |
A detecção de choque é o processo da determinação se os duendes tenham colidido um com outro. |
A detecção de choque usa-se para determinar quando os duendes fisicamente interagem um com outro. Em um jogo de Asteróides, por exemplo, se o duende de barco colidir com um duende de asteróide, o barco destrói-se. A detecção de choque é o mecanismo empregado para descobrir se o barco colidiu com o asteróide. Isto não poderia parecer uma grande coisa; somente compare as suas posições e ver se ficam sobrepostos, certo? Correto, mas consideram quantas comparações devem realizar-se quando muitos duendes se deslocam; cada duende deve ser em comparação com cada outro duende no sistema. Não é difícil ver como a de cima da detecção de choque eficaz pode ficar difícil de arranjar-se.
Não surpreendentemente, há muitas aproximações do manejo de detecção de choque. A aproximação mais simples é comparar os retângulos limitam de cada duende com os retângulos limitam de todos os outros duendes. Este método é eficiente, mas se tiver objetos que não são retangulares, certo grau do erro ocorre quando os objetos escovam um por outro. Isto é porque as esquinas poderiam ficar sobrepostas e indicar um choque quando realmente só as áreas transparentes ficam sobrepostas. Mais irregular a forma dos duendes, tipicamente ocorrem mais erros. A figura 24.1 mostra como o choque de retângulo simples trabalha.
A figura 24.1: detecção de choque usando choque de retângulo simples.
Na Figura 24.1 as áreas que determinam a detecção de choque sombreiam-se. Pode ver como a detecção de choque de retângulo simples não é muito exata a menos que esteja tratando com duendes que são retangulares na forma. Uma melhora nesta técnica deve encolher os retângulos de choque um pouco, que reduz o erro de esquina. Este método melhora coisas um pouco, mas tem o potencial de causar o erro na direção inversa permitindo a duendes ficar sobrepostos em alguns casos sem transmitir um choque. Não surpreendentemente, o choque de retângulo encolhido trabalha melhor quando trata com duendes que são rudemente circulares na forma.
A figura 24.2 mostra como encolher os retângulos de choque pode melhorar o erro na detecção de choque de retângulo simples. O choque de retângulo encolhido é tão eficiente como choque de retângulo simples porque tudo que faz compara retângulos da intersecção.
A figura 24.2: detecção de choque usando choque de retângulo encolhido.
A técnica de detecção de choque mais exata deve descobrir o choque baseado nos dados de imagem de duende, que implicam de fato a verificação se as partes transparentes do duende ou as próprias imagens de duende ficam sobrepostas. Neste caso, adquiriria um choque só se as imagens de duende reais ficam sobrepostas. Isto é a técnica ideal para descobrir choques porque é exato e permite aos objetos de qualquer forma de mover-se um por outro sem erro. A figura 24.3 mostra a detecção de choque usando os dados de imagem de duende.
A figura 24.3: detecção de choque usando dados de imagem de duende.
Infelizmente, esta técnica necessita muito mais de cima do que outros tipos da detecção de choque e muitas vezes é um gargalo principal na realização. Além disso, implementar detecção de choque de dados de imagem pode tornar-se muito confuso. Considerando estes fatos, enfocará os seus esforços depois hoje a implementação de dois primeiros tipos da detecção de choque.
Há um tópico último a cobrir antes de entrar nos detalhes da programação de animação em Java: rastreamento de imagens. Desde que as animações tipicamente necessitam múltiplas imagens, a questão de imagens gerentes quando se estão transferindo sobre uma conexão de Web não pode contemplar-se do alto. A questão primária com imagens que se transferem é a largura de banda limitada que muitos de nós têm com respeito às nossas conexões de Web. Desde que muitos de nós têm uma conexão de largura de banda limitada (pronunciado por modulador), a velocidade na qual as imagens se transferem sobre tal conexão de Web muitas vezes causa um atraso perceptível em Java applet confiante neles, como qualquer applet exposição de animações.
Há uma técnica padrão para tratar com o atraso de transferência como afeta imagens estáticas. Não há dúvida viu esta técnica no trabalho no seu Navegador da Web quando examinou imagens em Páginas da Web. A técnica conhece-se como entrelaçamento e faz imagens parecer manchadas até que se tenham completamente transferido. Para usar o entrelaçamento, as imagens devem guardar-se em um formato entrelaçado (normalmente a versão 89a de GIF), que significa que os dados de imagem se arranjam tais que a imagem pode expor-se antes que se transmita completamente. O entrelaçamento é uma boa aproximação do procedimento com atrasos de transmissão de imagens estáticas porque lhe permite ver a imagem quando se está transferindo. Sem entrelaçamento, tem de esperar até que a imagem inteira se tenha transferido antes que possa vê-lo em absoluto.
Antes que demasiado se excite sobre o entrelaçamento, deixe-me indicar que é só útil para imagens estáticas. Está admirando-se provavelmente porque este é o caso. Tem a ver com o fato que as animações (imagens dinâmicas) confiam rapidamente na exposição de uma sequência de imagens dentro de algum tempo, todas das quais devem estar prontamente disponíveis para criar com sucesso o efeito do movimento. Uma sequência de animação simplesmente não olharia o entrelaçamento de utilização direito porque algumas imagens se transfeririam antes de outros.
Uma boa solução para o problema de atraso da transferência em imagens animadas somente seria esperar até que todas as imagens se tenham transferido antes de expor a animação. Isto é perfeito, mas necessita que você saiba a posição de imagens quando se estão transferindo. Como pode saber possivelmente isto? Entre no perseguidor de meios de comunicação de Java.
O perseguidor de meios de comunicação de Java é um objeto que segue a pista quando os objetos de meios de comunicação, como imagens, se transferiram com sucesso. Usando o perseguidor de meios de comunicação, pode guardar a pista de qualquer número de objetos de meios de comunicação e questionar para ver quando terminaram de transmitir-se. Por exemplo, suponha que tem uma animação com quatro imagens. Registraria cada uma destas imagens com o perseguidor de meios de comunicação e logo esperaria até que se tenham todos transferido antes de expor a animação. O perseguidor de meios de comunicação acompanha a posição de carga de cada imagem. Quando o perseguidor de meios de comunicação informa que todas as imagens se carregaram com sucesso, garantem-lhe aquela animação sua tem todas as imagens necessárias para expor corretamente.
Java a classe de MediaTracker é parte do pacote awt e contém vários membros e métodos para seguir a pista de objetos de meios de comunicação. Infelizmente, a classe de MediaTracker que embarca com o lançamento 1.02 do Conjunto de Desenvolvedor de Java só apoia o rastreamento de imagem. Espera-se que as futuras versões de Java acrescentem o suporte de outros tipos de objetos de meios de comunicação como som e música.
A classe de MediaTracker fornece bandeiras de membro para representar vários estados associados com objetos de meios de comunicação seguidos a pista. Estas bandeiras devolvem-se por muitas das funções de membro de MediaTracker e são o seguinte:
A classe de MediaTracker fornece vários métodos para ajudar a seguir a pista de objetos de meios de comunicação:
MediaTracker(Component comp) - O construtor de MediaTracker toma um parâmetro único do tipo Component. Este parâmetro especifica o objeto de Component no qual as imagens seguidas a pista se desenharão consequentemente. Este parâmetro reflete a limitação atual de ser capaz só de seguir a pista de imagens com a classe de MediaTracker, e não sons ou outros tipos de meios de comunicação.
void addImage(Image image, int id) - O método de addImage acrescenta uma imagem à lista de imagens que atualmente se seguem a pista. Este método toma como o seu primeiro parâmetro um objeto de Image e como o seu segundo parâmetro um identificador que unicamente identifica a imagem. Se quiser seguir a pista de um grupo de imagens em conjunto, pode usar o mesmo identificador para cada imagem.
synchronized void addImage(Image image, int id, int w, int h) - Este método de addImage é semelhante ao primeiro, mas tem parâmetros adicionais para especificar a largura e a altura de uma imagem seguida a pista. Esta versão de addImage usa-se para seguir a pista de imagens que vai escalar; passa a largura e altura à qual escala a imagem.
boolean checkID(int id) - Depois que acrescentou imagens ao objeto de MediaTracker, está pronto para verificar a sua posição. Usa o método de checkID para verificar se as imagens que combinam com o identificador passado terminaram de carregar. O método de checkID devolve false se as imagens não tenham terminado de carregar, e true de outra maneira. Este método devolve true mesmo se o carregamento se tenha abortado ou se um erro tenha ocorrido. Deve chamar os métodos de verificação de erros apropriados para ver se um erro ocorreu. (Aprenderá sobre os métodos de verificação de erros um pouco depois nesta seção.) O método de checkID não carrega uma imagem se aquela imagem já não tenha começado a carregar.
synchronized boolean checkID(int id, boolean load) - Este método de checkID é semelhante ao primeiro exceto que lhe permite especificar que a imagem deve carregar-se mesmo se já não tenha começado a carregar, que se executa passando true no parâmetro de load.
boolean checkAll() - O método de checkAll é semelhante aos métodos de checkID, exceto que aplica a todas as imagens, não somente os que combinam com certo identificador. Os cheques de método de checkAll para ver se as imagens terminaram de carregar, mas não carregam nenhuma imagem que já não começou a carregar.
synchronized boolean checkAll(boolean load) - Este método de checkAll também verifica a posição de carregar imagens, mas lhe permite indicar que as imagens devem carregar-se se já não tenham começado.
void waitForID(int id) - usa o método de waitForID para começar a carregar imagens com certo identificador. Este identificador deve combinar com o identificador usado quando as imagens se acrescentaram ao perseguidor de meios de comunicação com o método de addImage. O método de waitForID é síncrono, significando que não volta até que todas as imagens especificadas tenham terminado de carregar ou um erro ocorre.
synchronized boolean waitForID(int id, long ms) - Este método de waitForID é semelhante ao primeiro exceto que lhe permite especificar um período de intervalo, em que caso a carga terminará e waitForID devolverá true. Especifica o período de intervalo em milissegundos usando o parâmetro de ms.
void waitForAll() - O método de waitForAll é semelhante aos métodos de waitForID, exceto que produz todas as imagens.
synchronized boolean waitForAll(long ms) - Este método de waitForAll é semelhante ao primeiro exceto que lhe permite especificar um período de intervalo, em que caso a carga terminará e waitForAll devolverá true. Especifica o período de intervalo em milissegundos usando o parâmetro de ms.
int statusID(int id, boolean load) - usa o método de statusID para decidir que a posição de imagens que combinam com o identificador passou no parâmetro de id. statusID devolve o OR bitwise das bandeiras de posição relacionadas às imagens. As bandeiras possíveis são LOADING, ABORTED, ERRORED e COMPLETE. O segundo parâmetro a statusID-load-should ser familiar para você por agora por causa do seu uso em outros meios de comunicação - métodos de perseguidor. Especifica se quer que as imagens comecem a carregar se já não tenham começado. Esta funcionalidade é semelhante a isto fornecido pelas segundas versões de métodos de waitForID e o checkID.
int statusAll(boolean load) - O método de statusAll é semelhante ao método de statusID; a única diferença é que statusAll devolve a posição de todas as imagens que se seguem a pista em vez de somente os que combinam com um identificador específico.
synchronized boolean isErrorID(int id) - O método de isErrorID verifica a posição incorreta de imagens que se seguem a pista, baseadas no argumento de identificador de id. Este método basicamente verifica a posição de cada imagem da bandeira de ERRORED. Observe que este método devolverá true se alguma das imagens tiver erros; isso é com você para determinar que imagens específicas tinham erros.
synchronized boolean isErrorAny() - O método de isErrorAny é semelhante ao método de isErrorID, exceto que verifica todas as imagens em vez de somente os que combinam com certo identificador. Como isErrorID, isErrorAny devolverá true se alguma das imagens tiver erros; isso é com você para determinar que imagens específicas tinham erros.
synchronized Object[] getErrorsID(int id) - Se usar isErrorID ou isErrorAny e descobrir que há erros de carga, tem de compreender que imagens têm erros. Faz isto usando o método de getErrorsID. Este método devolve uma tabela de Object s contendo os objetos de meios de comunicação que têm erros de carga. Na implementação atual da classe de MediaTracker, esta tabela sempre se enche de objetos de Image. Se não houver erros, este método devolve null.
synchronized Object[] getErrorsAny() - O método de getErrorsAny é muito semelhante a getErrorsID, exceto que devolve todas as imagens errored.
Isto enrola a descrição da classe de MediaTracker. Agora que entende sobre que a classe é tudo, está provavelmente pronto para vê-lo na ação. Não se incomode - a amostra de Tubarões applet vai se desenvolver depois hoje porá o perseguidor de meios de comunicação pelos seus passos.
Como aprendeu antes na lição de hoje, a animação de duende implica o movimento de objetos gráficos individuais chamados duendes. Diferentemente da animação de armação simples, a animação de duende implica um montante decente de em cima. Mais especificamente, é necessário desenvolver não só uma classe de duende, mas também uma classe de gestão de duende para acompanhar todos os duendes que criou. Isto é necessário porque os duendes têm de ser capazes de interagir um com outro por um mecanismo comum. Além disso, é bonito ser capaz de trabalhar com os duendes no conjunto quando vem a coisas como desenhar de fato os duendes na tela.
Nesta seção, aprenderá como implementar a animação de duende em Java criando uma suite de classes de duende. As classes de duende primárias são Sprite e SpriteVector. Contudo, também há algumas classes de suporte que aprenderá sobre como entra nos detalhes destas duas classes primárias. A classe de Sprite modela um duende único e contém toda a informação e métodos necessários para levantar um duende único e gerência. Contudo, a potência real da animação de duende arnesa-se combinando a classe de Sprite com a classe de SpriteVector, que é uma classe de container que dirige múltiplos duendes e a sua interação um com outro.
Embora os duendes possam implementar-se simplesmente como objetos gráficos móveis, mencionei antes que a classe de duende desenvolvida hoje também conterá o suporte da animação de armação. Um duende animado pela armação é basicamente um duende com múltiplas imagens de armação que podem expor-se na sucessão. A sua classe de Sprite apoiará a animação de armação na forma de uma tabela de imagens de armação e alguns métodos para estabelecer a imagem de armação que atualmente se expõe. Usando esta aproximação, terminará com uma classe de Sprite que apoia ambos os tipos fundamentais da animação, que lhe dá mais liberdade na criação de Java animado applets.
Antes de pular nos detalhes de como a classe de Sprite se implementa, tome um momento para pensar nas partes diferentes da informação que um duende deve acompanhar. Quando entender os componentes de um duende a um nível conceptual, será muito mais fácil entender o código de Java. Tão exatamente o que a informação a classe de Sprite deve manter? A seguinte lista contém a informação-chave que a classe de Sprite tem de incluir:
O primeiro componente, tabela de imagens de armação, é necessário para executar as animações de armação. Embora isto pareça que força um duende a ter múltiplas armações de animação, um duende também pode usar uma imagem única. Deste modo, os aspectos de animação de armação do duende são opcionais. A armação atual acompanha a armação atual da animação. Em um duende animado pela armação típico, a armação atual incrementa-se à seguinte armação quando o duende se atualiza.
A posição XY guarda a posição do duende. Move o duende simplesmente alterando esta posição. Alternativamente, pode estabelecer a velocidade e deixar o duende alterar a sua posição automaticamente baseada na velocidade.
A Z-ordem representa a profundidade do duende em relação a outros duendes. Enfim, a Z-ordem de um duende determina a sua ordem de desenho (aprenderá mais nisto um pouco depois).
Finalmente, o limite de um duende refere-se à região limitada para a qual o duende pode mover-se. Todos os duendes atam-se por alguma região normalmente o tamanho da janela applet. O limite de duende é importante porque determina os limites do movimento de um duende.
Agora que entende a informação principal necessitada pela classe de Sprite, é tempo de entrar na implementação de Java específica. Vamos começar com as variáveis de membro de classe de Sprite, que seguem:
public static final int BA_STOP = 0, BA_WRAP = 1, BA_BOUncE = 2, BA_DIE = 3; protected Component component; protected Image[] image; protected int frame, frameInc, frameDelay, frameTrigger; protected Rectangle position, collision; protected int zOrder; protected Point velocity; protected Rectangle bounds; protected int boundsAction; protected boolean hidden = false;
As variáveis de membro incluem a informação sobre duende importante mencionada antes, junto com um pouco de outra informação útil. O mais notavelmente, é provavelmente curioso dos membros finais estáticos no início da listagem. Estes membros são identificadores constantes que definem ações de limites do duende. As ações de limites são ações que um duende toma em resposta ao alcance de um limite, como empacotamento a outro lado ou ressalto. As ações de limites são mutuamente exclusivas, significando que só um pode estabelecer-se de uma vez.
A variável de membro de Component é necessária porque um objeto de ImageObserver deve recuperar a informação sobre uma imagem. Mas o que Component tem a ver com ImageObserver? A classe de Component implementa a interface de ImageObserver, e a classe de Applet consegue-se de Component. Portanto um objeto de Sprite adquire a sua informação sobre imagem de Java applet ele mesmo, que se usa para inicializar a variável de membro de Component.
Observar |
ImageObserver é uma interface definida no pacote de java.awt.image que fornece um meio para receber a informação sobre uma imagem. |
A variável de membro de image contém uma tabela de objetos de Image que representam as armações de animação do duende. Para duendes que não são armação animada, esta tabela simplesmente conterá um elemento.
A variável de membro de frameInc usa-se para fornecer um meio de modificar o modo que as armações de animação se atualizam. Por exemplo, em alguns casos poderia querer que as armações se expusessem na ordem inversa. Pode fazer facilmente isto estabelecendo frameInc em -1 (o seu valor típico é 1). O frameDelay e as variáveis de membro de frameTrigger usam-se para fornecer um meio de variar a velocidade da animação de armação. Verá como a velocidade da animação se controla quando aprende sobre o método de incFrame depois hoje.
A variável de membro de position é um objeto de Rectangle que representa a posição atual do duende. A variável de membro de collision também é um objeto de Rectangle e usa-se para apoiar a detecção de choque de retângulo. Verá como collision se usa depois na lição de hoje quando aprende sobre métodos de testCollision e o setCollision.
O zOrder e as variáveis de membro de velocity simplesmente guardam a Z-ordem e a velocidade do duende. A variável de membro de bounds representa o retângulo divisional ao qual o duende se limita, enquanto a variável de membro de boundsAction é a ação de limites que se toma quando o duende encontra o limite.
A variável de membro última, hidden, é uma bandeira booleana que determina se o duende se esconde. Estabelecendo esta variável em false, o duende esconde-se da visão. A sua colocação à revelia é true, significando que o duende é visível.
A classe de Sprite tem dois construtores. O primeiro construtor cria um Sprite sem suporte da animação de armação, subentendendo que usa uma imagem única para representar o duende. O código deste construtor segue:
public Sprite(Component comp, Image img, Point pos, Point vel, int z, int ba) { component = comp; image = new Image[1]; image[0] = img; setPosition(new Rectangle(pos.x, pos.y, img.getWidth(comp), img.getHeight(comp))); setVelocity(vel); frame = 0; frameInc = 0; frameDelay = frameTrigger = 0; zOrder = z; bounds = new Rectangle(0, 0, comp.size().width, comp.size().height); boundsAction = ba; }
Este construtor toma uma imagem, uma posição, uma velocidade, uma Z-ordem e uma ação divisional como parâmetros. O segundo construtor toma uma tabela de imagens e um pouco de informação adicional sobre as animações de armação. O código do segundo construtor segue:
public Sprite(Component comp, Image[] img, int f, int fi, int fd, Point pos, Point vel, int z, int ba) { component = comp; image = img; setPosition(new Rectangle(pos.x, pos.y, img[f].getWidth(comp), img[f].getHeight(comp))); setVelocity(vel); frame = f; frameInc = fi; frameDelay = frameTrigger = fd; zOrder = z; bounds = new Rectangle(0, 0, comp.size().width, comp.size().height); boundsAction = ba; }
A informação adicional necessitada deste construtor inclui a armação atual, incremento de armação e atraso de armação.
Aviso |
Como o parâmetro de armação, f, usado no segundo construtor de Sprite se usam de fato como um índice na tabela de imagens de armação, assegure-se que sempre o estabelece em um índice válido quando cria duendes usando este construtor. Em outras palavras, nunca passe um valor de armação que é do lado de fora dos limites da tabela de imagem. Na maioria dos casos usará um valor de armação de 0, que alivia o problema potencial. |
A classe de Sprite contém um número de métodos de acesso, que são simplesmente interfaces para adquirir e estabelecer certas variáveis de membro. Estes métodos compõem-se de uma ou duas linhas do código e são bastante evidentes. Verifique o código do getVelocity e métodos de acesso de setVelocity para ver o que quero dizer sobre os métodos de acesso que são evidentes:
public Point getVelocity() { return velocity; } public void setVelocity(Point vel) { velocity = vel; }
Há mais métodos de acesso para adquirir e estabelecer outras variáveis de membro em Sprite, mas são tão francos como getVelocity e setVelocity. Em vez de perder tempo naqueles, vamos mudar a alguns métodos mais interessantes!
O método de incFrame é o primeiro método de Sprite com qualquer verdadeira substância:
protected void incFrame() { if ((frameDelay > 0) && (--frameTrigger <= 0)) { // Reset the frame trigger frameTrigger = frameDelay; // Increment the frame frame += frameInc; if (frame >= image.length) frame = 0; else if (frame < 0) frame = image.length - 1; } }
incFrame usa-se para incrementar a armação de animação atual. Primeiro verifica o frameDelay e variáveis de membro de frameTrigger para ver se a armação deve incrementar-se de fato. Este cheque é o que lhe permite variar a velocidade de animação de armação de um duende, que se faz modificando o valor de frameDelay. Os valores maiores de frameDelay resultam em uma velocidade de animação mais lenta. A armação atual incrementa-se acrescentando frameInc a frame. frame então verifica-se para assegurar-se que o seu valor é dentro dos limites da tabela de imagem, porque se acostuma depois ao índice na tabela quando a imagem de armação se desenha.
Os métodos de setPosition estabelecem a posição do duende. O seu texto fonte segue:
void setPosition(Rectangle pos) { position = pos; setCollision(); } public void setPosition(Point pos) { position.move(pos.x, pos.y); setCollision(); }
Embora a posição de duende se guarde como um retângulo, os métodos de setPosition permitem-lhe especificar a posição de duende como um retângulo ou um ponto. Na última versão, o retângulo de posição simplesmente move-se para o ponto especificado. Depois que o retângulo de posição move-se, o retângulo de choque estabelece-se com uma chamada a setCollision. setCollision é o método que estabelece o retângulo de choque do duende. O texto fonte de setCollision segue:
protected void setCollision() { collision = position; }
Note que setCollision estabelece o retângulo de choque igual ao retângulo de posição, que resulta na detecção de choque de retângulo simples. Como não há modo de saber como que os duendes se formarão, deixa-o até classes de duende conseguidas para implementar versões de setCollision com cálculos de retângulo encolhidos específicos. Assim para implementar o choque de retângulo encolhido, somente calcula um mais pequeno retângulo de choque em setCollision.
Este método de isPointInside usa-se para testar se um ponto está dentro do duende. O texto fonte de isPointInside segue:
boolean isPointInside(Point pt) { return position.inside(pt.x, pt.y); }
Este método é prático para determinar se o usuário clicou em certo duende. Isto é útil em applets onde quer ser capaz de clicar em objetos e deslocá-los, como um jogo de xadrez. Em um jogo de xadrez, cada parte seria um duende, e usaria isPointInside para descobrir em que parte o usuário clicou.
O método que faz a maioria do trabalho em Sprite é o método de update, que se mostra na Listagem 24.1.
A listagem 24.1. A classe de Sprite método de update.
1: public boolean update() { 2: // Increment the frame 3: incFrame(); 4: 5: // Update the position 6: Point pos = new Point(position.x, position.y); 7: pos.translate(velocity.x, velocity.y); 8: 9: // Check the bounds 10: // Wrap? 11: if (boundsAction == Sprite.BA_WRAP) { 12: if ((pos.x + position.width) < bounds.x) 13: pos.x = bounds.x + bounds.width; 14: else if (pos.x > (bounds.x + bounds.width)) 15: pos.x = bounds.x - position.width; 16: if ((pos.y + position.height) < bounds.y) 17: pos.y = bounds.y + bounds.height; 18: else if (pos.y > (bounds.y + bounds.height)) 19: pos.y = bounds.y - position.height; 20: } 21: // Bounce? 22: else if (boundsAction == Sprite.BA_BOUncE) { 23: boolean bounce = false; 24: Point vel = new Point(velocity.x, velocity.y); 25: if (pos.x < bounds.x) { 26: bounce = true; 27: pos.x = bounds.x; 28: vel.x = -vel.x; 29: } 30: else if ((pos.x + position.width) > 31: (bounds.x + bounds.width)) { 32: bounce = true; 33: pos.x = bounds.x + bounds.width - position.width; 34: vel.x = -vel.x; 35: } 36: if (pos.y < bounds.y) { 37: bounce = true; 38: pos.y = bounds.y; 39: vel.y = -vel.y; 40: } 41: else if ((pos.y + position.height) > 42: (bounds.y + bounds.height)) { 43: bounce = true; 44: pos.y = bounds.y + bounds.height - position.height; 45: vel.y = -vel.y; 46: } 47: if (bounce) 48: setVelocity(vel); 49: } 50: // Die? 51: else if (boundsAction == Sprite.BA_DIE) { 52: if ((pos.x + position.width) < bounds.x || pos.x > bounds.width || 53: (pos.y + position.height) < bounds.y || pos.y > bounds.height) { 54: return true; 55: } 56: } 57: // Stop (default) 58: else { 59: if (pos.x < bounds.x || 60: pos.x > (bounds.x + bounds.width - position.width)) { 61: pos.x = Math.max(bounds.x, Math.min(pos.x, 62: bounds.x + bounds.width - position.width)); 63: setVelocity(new Point(0, 0)); 64: } 65: if (pos.y < bounds.y || 66: pos.y > (bounds.y + bounds.height - position.height)) { 67: pos.y = Math.max(bounds.y, Math.min(pos.y, 68: bounds.y + bounds.height - position.height)); 69: setVelocity(new Point(0, 0)); 70: } 71: } 72: setPosition(pos); 73: 74: return false; 75: }
Análise |
O método de update trata a tarefa de atualizar a armação de animação e a posição do duende. update começa atualizando a armação de animação com uma chamada a incFrame. A posição do duende então atualiza-se traduzindo o retângulo de posição baseado na velocidade. Pode pensar no retângulo de posição como se faz deslizar uma distância determinada pela velocidade. |
O resto do código em update dedica-se ao manejo de várias ações de limites. A primeira bandeira de ação de limites, BA_WRAP, faz que ao duende enrole em volta a outro lado do retângulo de limites. A bandeira de BA_BOUncE faz que ao duende salte se encontrar um limite. A bandeira de BA_DIE faz que ao duende morra se encontrar um limite. Finalmente, a bandeira à revelia, BA_STOP, faz que ao duende pare quando encontra um limite.
Note que update termina devolvendo um valor booleano. Este valor booleano especifica se o duende deve matar-se, que fornece um meio para duendes a destruir-se quando o BA_ DIE limita a ação se define. Se isto parecer um pouco estranho, tenha em mente que o único modo de livrar-se de um duende é retirá-lo do vetor de duende. Sei, não aprendeu muito sobre o vetor de duende ainda, mas confia em mim neste. Desde que os duendes individuais não sabem nada sobre o vetor de duende, não podem dizê-lo diretamente que fazer. Portanto o valor de retorno do método de update usa-se para comunicar-se ao vetor de duende se um duende tem de matar-se. Um regresso de true significa que o duende deve matar-se, e os meios de false deixam-no ser.
Observar |
O vetor de duende é a lista de todos os duendes atualmente no sistema de duende. É o vetor de duende que é responsável por dirigir todos os duendes, inclusive soma, remoção, desenho e descobrimento de choques entre eles. |
Julgando pelo seu tamanho, não é difícil compreender isto o método de update é a maior parte do código na classe de Sprite. Isto é lógico, entretanto, porque o método de update é onde toda a ação se realiza; update trata todos os detalhes de atualizar a armação de animação e a posição do duende, junto com a realização de ações de limites diferentes.
Outro método importante na classe de Sprite é draw, cujo texto fonte segue:
public void draw(Graphics g) { // Draw the current frame if (!hidden) g.drawImage(image[frame], position.x, position.y, component); }
Depois de passar com dificuldade pelo método de update, o método de draw parece a uma parte do bolo! Simplesmente usa o método de drawImage para desenhar a imagem de armação de duende atual ao objeto de Graphics que se passa em. Note que o método de drawImage necessita que a imagem, posição de XY e componente (ImageObserver) executem isto.
O método último em Sprite é testCollision, que se usa para verificar choques entre duendes:
protected boolean testCollision(Sprite test) { // Check for collision with another sprite if (test != this) return collision.intersects(test.getCollision()); return false; }
O duende para testar para o choque passa-se no parâmetro de test. O teste simplesmente implica a verificação se os retângulos de choque se cruzam. Nesse caso testCollision devolve true. testCollision não é tudo que útil dentro do contexto de um duende único, mas é muito prático quando junta a classe de SpriteVector, que vai fazer depois.
Neste ponto, tem uma classe de Sprite com algumas características bastante impressionantes, mas realmente não tem modo de dirigi-lo. Naturalmente, pode progredir e criar um applet com alguns objetos de Sprite, mas como seriam capazes de interagir um com outro? A resposta a esta pergunta é a classe de SpriteVector, que trata todos os detalhes de manter uma lista de duendes e tratar as interações entre eles.
A classe de SpriteVector consegue-se da classe de Vector, que é uma classe padrão fornecida no pacote de java.util. A classe de Vector modela uma tabela growable de objetos. Neste caso, a classe de SpriteVector usa-se como um container para uma tabela growable de objetos de Sprite.
A classe de SpriteVector tem só uma variável de membro, background, que é um objeto de Background:
protected Background background;
Este objeto de Background representa o contexto no qual os duendes aparecem. Inicializa-se no construtor de SpriteVector, como isto:
public SpriteVector(Background back) { super(50, 10); background = back; }
O construtor de SpriteVector simplesmente toma um objeto de Background como o seu único parâmetro. Aprenderá sobre a classe de Background um pouco depois hoje. Note que o construtor de SpriteVector chama o construtor de classe de pai de Vector e faz que a capacidade de expansão à revelia (50) e montante incremente a capacidade de expansão (10) se o vetor tiver de crescer.
SpriteVector contém dois métodos de acesso para adquirir e estabelecer a variável de membro de background, que seguem:
public Background getBackground() { return background; } public void setBackground(Background back) { background = back; }
Estes métodos são úteis sempre que tenha uma animação que tem de ter um contexto que se modifica. Para modificar o contexto, simplesmente chama setBackground e passa no novo objeto de Background.
O método de getEmptyPosition usa-se pela classe de SpriteVector para ajudar a posicionar novos duendes. A listagem 24.2 contém o texto fonte de getEmptyPosition.
A listagem 24.2. A classe de SpriteVector método de getEmptyPosition.
1: public Point getEmptyPosition(Dimension sSize) { 2: Rectangle pos = new Rectangle(0, 0, sSize.width, sSize.height); 3: Random rand = new Random(System.currentTimeMillis()); 4: boolean empty = false; 5: int numTries = 0; 6: 7: // Look for an empty position 8: while (!empty && numTries++ < 50) { 9: // Get a random position 10: pos.x = Math.abs(rand.nextInt() % 11: background.getSize().width); 12: pos.y = Math.abs(rand.nextInt() % 13: background.getSize().height); 14: 15: // Iterate through sprites, checking if position is empty 16: boolean collision = false; 17: for (int i = 0; i < size(); i++) { 18: Rectangle testPos = ((Sprite)elementAt(i)).getPosition(); 19: if (pos.intersects(testPos)) { 20: collision = true; 21: break; 22: } 23: } 24: empty = !collision; 25: } 26: return new Point(pos.x, pos.y); 27: }
Análise |
getEmptyPosition é um método cuja importância não poderia ser prontamente evidente para você agora mesmo; usa-se para encontrar uma posição física vazia na qual colocar um novo duende no vetor de duende. Isto não significa a posição do duende na tabela; melhor significa a sua posição física quanto à tela. Este método é útil quando quer colocar à toa múltiplos duendes na tela. Usando getEmptyPosition, elimina a possibilidade de colocar novos duendes em cima de duendes existentes. |
O método de isPointInside em SpriteVector é semelhante à versão de isPointInside em Sprite, exceto ele atravessa o vetor de duende inteiro, verificando cada duende. Verifique o texto fonte para ele:
Sprite isPointInside(Point pt) { // Iterate backward through the sprites, testing each for (int i = (size() - 1); i >= 0; i--) { Sprite s = (Sprite)elementAt(i); if (s.isPointInside(pt)) return s; } return null; }
Se o ponto passou no parâmetro pt está em um duende, isPointInside devolve o duende. Note que o vetor de duende se procura ao contrário, significando que o duende último se verifica antes do primeiro. Os duendes procuram-se nesta ordem por uma razão muito importante: Z-ordem. Os duendes guardam-se no vetor de duende classificado na Z-ordem que ascende, que especifica a sua profundidade na tela. Por isso, os duendes perto do começo da lista escondem-se às vezes por duendes perto do fim da lista. Se quiser verificar um ponto que está dentro de um duende, faz sentido para verificar os duendes o mais altos primeiro - isto é, os duendes com valores de Z-ordem maiores. Se isto parecer um pouco confuso, não se incomode; aprenderá mais sobre a Z-ordem depois hoje quando vier ao método de add.
Como em Sprite, o método de update é o método-chave em SpriteVector porque trata a atualização de todos os duendes. A listagem 24.3 contém o texto fonte de update.
A listagem 24.3. A classe de SpriteVector método de update.
1: public void update() { 2: // Iterate through sprites, updating each 3: Sprite s, sHit; 4: Rectangle lastPos; 5: for (int i = 0; i < size(); ) { 6: // Update the sprite 7: s = (Sprite)elementAt(i); 8: lastPos = new Rectangle(s.getPosition().x, s.getPosition().y, 9: s.getPosition().width, s.getPosition().height); 10: boolean kill = s.update(); 11: 12: // Should the sprite die? 13: if (kill) { 14: removeElementAt(i); 15: continue; 16: } 17: 18: // Test for collision 19: int iHit = testCollision(s); 20: if (iHit >= 0) 21: if (collision(i, iHit)) 22: s.setPosition(lastPos); 23: i++; 24: } 25: }
Análise |
O método de update repete pelos duendes, chamando o método de update DE Sprite em cada um. Então verifica o valor de retorno de update para ver se o duende deve matar-se. Se o valor de retorno for true, o duende retira-se do vetor de duende. Finalmente, testCollision chama-se para ver se um choque ocorreu entre duendes. (Adquire a pá inteira em testCollision durante um momento.) Se um choque tenha ocorrido, a velha posição do duende colidido restaura-se e o método de collision chama-se. |
O método de collision usa-se para tratar choques entre dois duendes:
protected boolean collision(int i, int iHit) { // Do nothing return false; }
O método de collision é responsável por tratar qualquer ação que resulta de um choque entre duendes. A ação neste caso simplesmente não deve fazer nada, que permite a duendes passar um sobre outro com nada o acontecimento. Este método é onde fornece ações de choque específicas em duendes conseguidos. Por exemplo, em uma animação de simulador do tempo, poderia querer que nuvens causassem o relâmpago quando colidem.
O método de testCollision usa-se para testar para choques entre um duende e o resto dos duendes no vetor de duende:
protected int testCollision(Sprite test) { // Check for collision with other sprites Sprite s; for (int i = 0; i < size(); i++) { s = (Sprite)elementAt(i); if (s == test) // don't check itself continue; if (test.testCollision(s)) return i; } return -1; }
O duende a testar-se passa-se no parâmetro de test. Os duendes então repetem-se por, e o método de testCollision em Sprite chama-se para cada um. Note que testCollision não se chama no duende de experiência se a iteração se referir ao mesmo duende. Para entender a significação deste código, considere o efeito de passar testCollision o mesmo duende o método está convidando-se; estaria verificando para ver se um duende colidia com si mesmo, que sempre devolveria true. Se um choque se descobrir, o objeto de Sprite que se bateu devolve-se de testCollision.
As maçanetas de método de draw que desenham o contexto, bem como desenham todos os duendes:
public void draw(Graphics g) { // Draw the background background.draw(g); // Iterate through sprites, drawing each for (int i = 0; i < size(); i++) ((Sprite)elementAt(i)).draw(g); }
O contexto desenha-se com uma chamada simples ao método de draw do objeto de Background. Os duendes então desenham-se repetindo pelo vetor de duende e chamando o método de draw para cada um.
O método de add é provavelmente o método mais enganador na classe de SpriteVector. A listagem 24.4 contém o texto fonte de add.
A listagem 24.4. A classe de SpriteVector método de add.
1: public int add(Sprite s) { 2: // Use a binary search to find the right location to insert the 3: // new sprite (based on z-order) 4: int l = 0, r = size(), i = 0; 5: int z = s.getZOrder(), 6: zTest = z + 1; 7: while (r > l) { 8: i = (l + r) / 2; 9: zTest = ((Sprite)elementAt(i)).getZOrder(); 10: if (z < zTest) 11: r = i; 12: else 13: l = i + 1; 14: if (z == zTest) 15: break; 16: } 17: if (z >= zTest) 18: i++; 19: 20: insertElementAt(s, i); 21: return i; 22: }
Análise |
As maçanetas de método de add que acrescentam novos duendes ao vetor de duende. O proveito é que o vetor de duende sempre deve classificar-se segundo a Z-ordem. Porque isto é? Lembre-se de que a Z-ordem é a profundidade na qual os duendes aparecem na tela. A ilusão da profundidade estabelece-se pela ordem na qual os duendes se desenham. Isto trabalha porque os duendes desenhados depois se desenham em cima de duendes desenhados antes, e por isso parecem estar em uma profundidade mais alta. Por isso, classificar o vetor de duende subindo a Z-ordem e logo desenhando-os naquela ordem é um modo eficaz de fornecer a ilusão da profundidade. O método de add usa uma pesquisa binária para encontrar que o lugar direito acrescenta novos duendes para que o vetor de duende permaneça classificado pela Z-ordem. |
Isto enrola a classe de SpriteVector! Agora não tem só uma classe de Sprite potente, mas também uma classe de SpriteVector para dirigir e fornecer a interatividade entre duendes. Tudo isto deixa-se põe estas classes para trabalhar em um verdadeiro applet.
De fato, há algum negócio inacabado para tratar antes que prove as classes de duende. Refiro-me à classe de Background usada em SpriteVector. Enquanto está nele, vamos adiante e olha para algumas classes de fundo diferentes que poderia achar prático.
Se lembra, mencionei antes hoje que a classe de Background fornece o de cima de dirigir um contexto dos duendes para aparecer em cima de. O texto fonte da classe de Background mostra-se na Listagem 24.5.
A listagem 24.5. A classe de Background.
1: public class Background { 2: protected Component component; 3: protected Dimension size; 4: 5: public Background(Component comp) { 6: component = comp; 7: size = comp.size(); 8: } 9: 10: public Dimension getSize() { 11: return size; 12: } 13: 14: public void draw(Graphics g) { 15: // Fill with component color 16: g.setColor(component.getBackground()); 17: g.fillRect(0, 0, size.width, size.height); 18: g.setColor(Color.black); 19: } 20: }
Análise |
Como pode ver, a classe de Background é bastante simples. Basicamente fornece uma abstração limpa do contexto dos duendes. As duas variáveis de membro mantidas por Background usam-se para acompanhar o componente associado e dimensões do contexto. O construtor de Background toma um objeto de Component como o seu único parâmetro. Este objeto de Component é tipicamente a janela applet, e serve para fornecer as dimensões do contexto e a cor de contexto à revelia. |
O método de getSize é um método de acesso que simplesmente devolve o tamanho do contexto. O método de draw enche o contexto da cor de contexto à revelia, como definido pela variável de membro de component.
Está pensando provavelmente que este objeto de Background não é demasiado excitante. Somente não pode picar este código de desenho diretamente no método de sorteio de SpriteVector? Sim, poderia, mas então perderia os benefícios fornecidos pelas classes de fundo mais conseguidas, ColorBackground e ImageBackground, que se explicam depois. As classes de fundo são um bom exemplo de como o desenho orientado ao objeto faz Java codificar muito mais limpo e mais fácil estender-se.
A classe de ColorBackground fornece um contexto que pode encher-se de qualquer cor. A listagem 24.6 contém o texto fonte da classe de ColorBackground.
A listagem 24.6. A classe de ColorBackground.
1: public class ColorBackground extends Background { 2: protected Color color; 3: 4: public ColorBackground(Component comp, Color c) { 5: super(comp); 6: color = c; 7: } 8: 9: public Color getColor() { 10: return color; 11: } 12: 13: public void setColor(Color c) { 14: color = c; 15: } 16: 17: public void draw(Graphics g) { 18: // Fill with color 19: g.setColor(color); 20: g.fillRect(0, 0, size.width, size.height); 21: g.setColor(Color.black); 22: } 23: }
Análise |
ColorBackground acrescenta uma variável de membro única, color, que é um objeto de Color. Esta variável de membro considera que a cor costumou encher o contexto. O construtor de ColorBackground toma Component e objetos de Color como parâmetros. Há dois métodos de acesso para adquirir e estabelecer a variável de membro de color. O método de draw de ColorBackground é muito semelhante ao método de draw em Background, exceto que a variável de membro de color se usa como a cor encher. |
Uma classe derivada de Background mais interessante é ImageBackground, que usa uma imagem como o contexto. A listagem 24.7 contém o texto fonte da classe de ImageBackground.
A listagem 24.7. A classe de ImageBackground.
1: public class ImageBackground extends Background { 2: protected Image image; 3: 4: public ImageBackground(Component comp, Image img) { 5: super(comp); 6: image = img; 7: } 8: 9: public Image getImage() { 10: return image; 11: } 12: 13: public void setImage(Image img) { 14: image = img; 15: } 16: 17: public void draw(Graphics g) { 18: // Draw background image 19: g.drawImage(image, 0, 0, component); 20: } 21: }
Análise |
A classe de ImageBackground acrescenta uma variável de membro única, image, que é um objeto de Image . Esta variável de membro considera a imagem usar-se como o contexto. Não surpreendentemente, o construtor de ImageBackground toma Component e objetos de Image como parâmetros. Há dois métodos de acesso para adquirir e estabelecer a variável de membro de image . O método de draw de ImageBackground simplesmente desenha a imagem de fundo usando o método de drawImage do objeto de Graphics passado. |
É tempo de tomar todo o trabalho duro que pôs nas classes de duende e vê para que sobe. A figura 24.4 mostra uma captura de tela dos Tubarões applet, que exibe as classes de duende nas quais trabalhou tão muito todo o dia. O texto fonte completo, as imagens e as classes executáveis dos Tubarões applet estão no CD-ROM acompanhante.
A figura 24.4: Os Tubarões applet.
Os Tubarões applet usam um objeto de SpriteVector de dirigir um grupo de tubarão com fome objetos de Sprite. Este objeto, sv, é um do Shark applet as variáveis de membro de classe, que seguem:
private Image offImage, back; private Image[] leftShark = new Image[2]; private Image[] rightShark = new Image[2]; private Image[] clouds = new Image[2]; private Graphics offGrfx; private Thread animate; private MediaTracker tracker; private SpriteVector sv; private int delay = 83; // 12 fps private Random rand = new Random(System.currentTimeMillis());
As variáveis de membro de Image na classe de Sharks representam o fora da tela buffer, a imagem de fundo, as imagens de tubarão e algumas imagens de nuvem. A variável de membro de Graphics, offGrfx, mantém o contexto de gráficos da imagem fora da tela de buffers. A variável de membro de Thread, animate, usa-se para manter o fio onde a animação se realiza. A variável de membro de MediaTracker, tracker, usa-se para seguir a pista de várias imagens como se estão carregando. A variável de membro de SpriteVector, sv, mantém o vetor de duende do applet. O número inteiro (int) variável de membro, delay, determina a velocidade de animação dos duendes. Finalmente, a variável de membro de Random, rand, usa-se para gerar números aleatórios em todas as partes do applet.
Note que a variável de membro de delay se estabelece em 83. A variável de membro de delay especifica o período de tempo (em milissegundos) que passa entre cada armação da animação. Pode determinar a tarifa de armação invertendo o valor de delay, que resulta em uma tarifa de armação de aproximadamente 12 armações por segundo (fps) neste caso. Esta tarifa de armação é bastante bem a tarifa mínima necessitada para a animação fluida, como animação de duende. Verá como delay se usa para estabelecer a tarifa de armação durante um momento quando entra nos detalhes do método de run.
A classe de Sharks método de init carrega todas as imagens e registra-os com o perseguidor de meios de comunicação:
public void init() { // Load and track the images tracker = new MediaTracker(this); back = getImage(getCodeBase(), "Water.gif"); tracker.addImage(back, 0); leftShark[0] = getImage(getCodeBase(), "LShark0.gif"); tracker.addImage(leftShark[0], 0); leftShark[1] = getImage(getCodeBase(), "LShark1.gif"); tracker.addImage(leftShark[1], 0); rightShark[0] = getImage(getCodeBase(), "RShark0.gif"); tracker.addImage(rightShark[0], 0); rightShark[1] = getImage(getCodeBase(), "RShark1.gif"); tracker.addImage(rightShark[1], 0); clouds[0] = getImage(getCodeBase(), "SmCloud.gif"); tracker.addImage(clouds[0], 0); clouds[1] = getImage(getCodeBase(), "LgCloud.gif"); tracker.addImage(clouds[1], 0); }
O rastreamento das imagens é necessário porque quer esperar até que todas as imagens se tenham carregado antes que comece a animação. O start e os métodos de stop são o fio padrão - métodos de treinador:
public void start() { if (animate == null) { animate = new Thread(this); animate.start(); } } public void stop() { if (animate != null) { animate.stop(); animate = null; } }
O método de start é responsável por inicializar e começar o fio de animação. De mesmo modo, o método de stop para o fio de animação e limpa depois dele.
Aviso |
Se por alguma razão estiver pensando que parar o fio de animação no método de stop não é realmente tão grande de um acordo, pense novamente. O método de stop chama-se sempre que um usuário deixe a Página da Web que contém um applet, em que caso tem a grande importância que para toda a execução de fios no applet. Por isso sempre se assegure para parar fios no método de stop do seu applets. |
O método de run é o coração do fio de animação. A listagem 24.8 mostra o texto fonte
para run.
A listagem 24.8. A classe de Sharks método de run.
1: public void run() { 2: try { 3: tracker.waitForID(0); 4: } 5: catch (InterruptedException e) { 6: return; 7: } 8: 9: // Create the sprite vector 10: sv = new SpriteVector(new ImageBackground(this, back)); 11: 12: // Create and add the sharks 13: for (int i = 0; i < 8; i++) { 14: boolean left = (rand.nextInt() % 2 == 0); 15: Point pos = new Point(Math.abs(rand.nextInt() % size().width), 16: (i + 1) * 4 + i * leftShark[0].getHeight(this)); 17: Sprite s = new Sprite(this, left ? leftShark: rightShark, 0, 1, 3, 18: pos, new Point((Math.abs(rand.nextInt() % 3) + 1) * (left ? -1: 1), 19: 0), 0, Sprite.BA_WRAP); 20: sv.add(s); 21: } 22: 23: // Create and add the clouds 24: Sprite s = new Sprite(this, clouds[0], new Point(Math.abs(rand.nextInt() 25: % size().width), Math.abs(rand.nextInt() % size().height)), new 26: Point(Math.abs(rand.nextInt() % 5) + 1, rand.nextInt() % 3), 1, 27: Sprite.BA_WRAP); 28: sv.add(s); 29: s = new Sprite(this, clouds[1], new Point(Math.abs(rand.nextInt() 30: % size().width), Math.abs(rand.nextInt() % size().height)), new 31: Point(Math.abs(rand.nextInt() % 5) - 5, rand.nextInt() % 3), 2, 32: Sprite.BA_WRAP); 33: sv.add(s); 34: 35: // Update everything 36: long t = System.currentTimeMillis(); 37: while (Thread.currentThread() == animate) { 38: // Update the sprites 39: sv.update(); 40: repaint(); 41: try { 42: t += delay; 43: Thread.sleep(Math.max(0, t - System.currentTimeMillis())); 44: } 45: catch (InterruptedException e) { 46: break; 47: } 48: } 49: }
Análise |
O método de run primeiro espera pelas imagens para terminar de carregar chamando o método de waitForID do objeto de MediaTracker. Depois que as imagens terminaram de carregar, o SpriteVector cria-se. Oito tubarão diferente objetos de Sprite então cria-se com posições variadas quanto à tela. Também note que a direção que cada tubarão move se escolhe à toa, como são as imagens de tubarão, que também refletem a direção. Estes duendes de tubarão então acrescentam-se ao vetor de duende. |
Uma vez que os tubarões acrescentaram-se, uns duendes de nuvem de par acrescentam-se somente para fazer coisas um pouco mais interessantes. Note que a Z-ordem das nuvens é maior do que aquele dos tubarões. A Z-ordem é o parâmetro penúltimo no construtor de Sprite e estabelece-se em 1 e 2 das nuvens e 0 dos tubarões. Isto resulta nas nuvens que aparecem em cima dos tubarões, como devem. Também, a nuvem com uma Z-ordem de 2 parecerá estar acima da nuvem com uma Z-ordem de 1 se deverem passar um a outro.
Depois de criar e acrescentar as nuvens, um laço de while introduz-se que maçanetas que atualizam o SpriteVector e forçam o applet a repintar-se. Forçando um repinte, faz que ao applet desenhe novamente os duendes nos seus estados recentemente atualizados.
Antes que mude, é importante entender como a tarifa de armação se controla no método de run. A chamada a currentTimeMillis devolve o tempo de sistema atual em milissegundos. Realmente não se preocupa com que tempo absoluto este método volta, porque só o usa aqui para medir o tempo relativo. Depois de atualizar os duendes e forçar desenhar novamente, o valor de delay acrescenta-se ao tempo que somente recuperou. Neste ponto, atualizou a armação e calculou um valor temporal que é milissegundos de delay no futuro. O seguinte passo deve dizer ao fio de animação dormir um período de tempo igual à diferença entre o futuro valor temporal que somente calculou e o tempo presente.
Isto provavelmente parece bastante confuso, então deixe-me clarificar coisas um pouco. O método de sleep usa-se para fazer um sono de fio de um número de milissegundos, como determinado pelo valor passado no seu único parâmetro. Poderia pensar que somente pode passar delay a sleep e as coisas seriam perfeitas. Esta aproximação tecnicamente trabalharia, mas teria certo grau do erro. A razão consiste em que um período de tempo finito passa entre atualização dos duendes e colocação do fio para dormir. Sem prestar contas deste tempo perdido, o atraso real entre armações não seria igual ao valor de delay. A solução é verificar o tempo antes e depois que os duendes se atualizam, e logo refletem que a diferença no valor de atraso passou ao método de sleep. E isto é como a tarifa de armação se dirige!
O método de update é onde os duendes se desenham de fato à janela applet:
public void update(Graphics g) { // Create the offscreen graphics context if (offGrfx == null) { offImage = createImage(size().width, size().height); offGrfx = offImage.getGraphics(); } // Draw the sprites sv.draw(offGrfx); // Draw the image onto the screen g.drawImage(offImage, 0, 0, null); }
O método de update usa duplo armazenar em buffer para eliminar o bruxuleio na animação de duende. Usando duplo armazenar em buffer, elimina o bruxuleio e leva em conta animações mais rápidas. A variável de membro de offImage contém a imagem fora da tela de buffers usada para desenhar a seguinte armação de animação. A variável de membro de offGrfx contém o contexto de gráficos associado com a imagem fora da tela de buffers.
Em update, fora da tela o buffer cria-se primeiro como um objeto de Image cujas dimensões combinam com aqueles da janela applet. É importante que fora da tela armazenem em buffer ser exatamente o mesmo tamanho que a janela applet. O contexto de gráficos associado com o buffer então recupera-se usando o método de getGraphics de Image. Depois fora da tela o buffer inicializa-se, tudo que realmente tem de fazer é contam ao objeto de SpriteVector de desenhar-se ao buffer. Lembre-se de que o objeto de SpriteVector cuida do desenho do contexto e todos os duendes. Isto realiza-se com uma chamada simples ao método de draw DE SpriteVector. Fora da tela o buffer então desenha-se à janela applet usando o método de drawImage.
Embora o método de update cuide do desenho de tudo, ainda é importante implementar o método de paint. Em verdade, o método de paint é muito útil no fornecimento do usuário o feedback visual quanto ao estado das imagens usadas pelo applet. A listagem 24.9 mostra o texto fonte de paint.
A listagem 24.9. A classe de Sharks método de paint.
1: public void paint(Graphics g) { 2: if ((tracker.statusID(0, true) & MediaTracker.ERRORED) != 0) { 3: // Draw the error rectangle 4: g.setColor(Color.red); 5: g.fillRect(0, 0, size().width, size().height); 6: return; 7: } 8: if ((tracker.statusID(0, true) & MediaTracker.COMPLETE) != 0) { 9: // Draw the offscreen image 10: g.drawImage(offImage, 0, 0, null); 11: } 12: else { 13: // Draw the title message (while the images load) 14: Font f1 = new Font("TimesRoman", Font.BOLD, 28), 15: f2 = new Font("Helvetica", Font.PLAIN, 16); 16: FontMetrics fm1 = g.getFontMetrics(f1), 17: fm2 = g.getFontMetrics(f2); 18: String s1 = new String("Sharks"), 19: s2 = new String("Loading images..."); 20: g.setFont(f1); 21: g.drawString(s1, (size().width - fm1.stringWidth(s1)) / 2, 22: ((size().height - fm1.getHeight()) / 2) + fm1.getAscent()); 23: g.setFont(f2); 24: g.drawString(s2, (size().width - fm2.stringWidth(s2)) / 2, 25: size().height - fm2.getHeight() - fm2.getAscent()); 26: } 27: }
Análise |
Usando o perseguidor de meios de comunicação, paint notifica o usuário que as imagens ainda carregam, ou que um erro ocorreu carregando-os. A Figura 24.5 de check out, que mostra os Tubarões applet enquanto as imagens carregam. |
A figura 24.5: Os Tubarões applet enquanto as imagens carregam.
Se um erro ocorrer carregando uma das imagens, o método de paint expõe um retângulo vermelho sobre a área de janela applet inteira. Se as imagens tenham terminado de carregar, paint somente desenha o último fora da tela armazenam em buffer à janela applet. Se as imagens não tenham terminado de carregar, paint expõe o título do applet e uma mensagem afirmando que as imagens ainda carregam (ver a Figura 24.5). Expor o título e mensagem de posição compõe-se de criar as fontes apropriadas e centrar o texto dentro da janela applet.
Isto é tudo que precisa para adquirir a colaboração de classes de duende. Poderia parecer muito código no início, mas pensar em tudo que o applet empreende. O applet é responsável por carregar e guardar a pista de todas as imagens usadas pelos duendes, bem como o contexto e fora da tela armazene em buffer. Se as imagens não tenham terminado de carregar, ou se um erro ocorrer carregando, o applet tem de notificar o usuário consequentemente. Adicionalmente, o applet é responsável por manter uma tarifa de armação consistente e desenhar os duendes usando duplo armazenar em buffer. Mesmo com estas responsabilidades, o applet ainda se beneficia muito da funcionalidade fornecida pelas classes de duende.
Pode usar este applet como um padrão applet para outro applets cria aquele uso as classes de duende. Agora tem toda a funcionalidade necessitada dirigir tanto a forma - como a animação baseada na armação, bem como fornecer o suporte da interatividade entre duendes via a detecção de choque.
Na lição de hoje aprendeu todos sobre a animação, inclusive dois tipos principais da animação: a armação baseou e lançou baseado. Acrescentando a esta teoria, aprendeu que a animação de duende consiste em onde a ação realmente é. Viu diretamente como desenvolver um duo potente de classes de duende para implementar a animação de duende, inclusive algumas classes de suporte para fazer coisas mais fáceis. Pôs as classes de duende para trabalhar em uma amostra applet que implica relativamente pequeno adicional em cima.
Agora tem tudo que tem de começar a criar as suas próprias animações de duende de Java com a tranquilidade. Se isto não for bastante para você, somente espere até a lição de amanhã, que trata com outro tópico de gráficos promovido: filtros de imagem.
Qual é a grande coisa com duendes? | |
A grande coisa consiste em que os duendes fornecem uma aproximação muito flexível da implementação de animação. Adicionalmente, utilização de duendes pode aproveitar-se de ambos os tipos fundamentais da animação: animação baseada na armação e animação baseada na forma. | |
O que exatamente é Z-ordem, e realmente preciso dela? | |
A Z-ordem é a profundidade de um duende quanto a outros duendes; os duendes com valores de Z-ordem mais altos parecem ser em cima de duendes com valores de Z-ordem mais baixos. Só precisa da Z-ordem se tiver duendes que ficam sobrepostos um a outro, em que caso a Z-ordem determinará que esconde o outro. | |
Porque preocupação com os tipos diferentes de detecção de choque? | |
Os tipos diferentes da detecção de choque (retângulo, retângulo encolhido e dados de imagem) fornecem trocas diferentes com respeito a realização e exatidão. O retângulo e a detecção de choque de retângulo encolhida fornecem uma solução muito de alta performance, mas com o moderado com a exatidão má. A detecção de choque dos dados de imagem é perfeita quando vem à exatidão, mas pode trazer o seu applet aos seus joelhos no departamento de realização, sem falar de dão-lhe uma dor de cabeça que tenta fazê-lo trabalhar. | |
Porque preciso da classe de SpriteVector? Não é a classe de Sprite bastante? | |
A classe de Sprite é bonita, mas representa só um duende único. Para permitir a múltiplos duendes interagir um com outro, deve ter uma segunda entidade que substitui como uma unidade de armazenamento sob os duendes. A classe de SpriteVector resolve este problema dobrando-se como um container de todos os duendes bem como um meio de descobrir choques entre duendes. |