Obtendo recursos embutidos em JARs com NIO.2

Digamos que no nosso projeto temos a seguinte estrutura:

.
└── src
    ├── meu-pacote
    │   └── nio2
    │       └── MinhaClasse.java
    └── config.xml

Dentro de MinhaClasse, estou interessado em obter o conteúdo do arquivo config.xml.

Para isso:

  • obtemos a URI do recurso através do método getResource de nosso classe seguido ao método toURI
  • utilizamos o método get da classe auxiliar Paths para obter um Path a partir da URI
  • criamos uma String a partir dos bytes retornados pelo método readAllBytes da classe auxiliar Files
public class MinhaClasse {
  public static void main(String[] args) throws URISyntaxException, IOException {

    URI uriDoRecurso = MinhaClasse.class.getResource("/config.xml").toURI();
    Path pathDoRecurso = Paths.get(uriDoRecurso);
    String conteudoDoRecurso = new String(Files.readAllBytes(pathDoRecurso));
    System.out.println(conteudoDoRecurso);

  }
}

Ao executarmos o código anterior de dentro de nossa IDE, o código tem o efeito esperado. Tudo funciona!

Mas e se exportarmos para um JAR?

Digamos que nosso JAR seja chamado minha-lib.jar. Para que a nossa classe seja executada por padrão na execução do JAR, conteria a configuração Main-Class do arquivo META-INF/MANIFEST.MF.

O conteúdo de minha-lib.jar seria o seguinte:

.
├── meu-pacote
│   └── nio2
│       └── MinhaClasse.class
├── config.xml
├── META-INF
│   └── MANIFEST.MF

Poderíamos então executar o JAR com o comando:

java -jar minha-lib.jar

Teríamos como resultado a seguinte exceção:

Exception in thread "main" java.nio.file.FileSystemNotFoundException
	at com.sun.nio.zipfs.ZipFileSystemProvider.getFileSystem(ZipFileSystemProvider.java:171)
	at com.sun.nio.zipfs.ZipFileSystemProvider.getPath(ZipFileSystemProvider.java:157)
	at java.nio.file.Paths.get(Paths.java:143)
	at meu-pacote.nio2.MinhaClasse.main(MinhaClasse.java:14)

Porque será que foi lançada a exceção FileSystemNotFoundException?

O que acontece é que um recurso que está embutido dentro de um JAR tem uma URI diferente. No nosso caso, seria parecida com: jar:file:/home/alexandre/minha-lib.jar!/config.xml

Observe que temos primeiramente o caminho até o JAR e, depois do !, o caminho do recurso que está embutido dentro do JAR.

Nossa primeira alteração no programa deve ser a detecção que temos um recurso de um JAR. Podemos fazer isso verificando se há o caracter !:

URI uriDoRecurso = MinhaClasse.class.getResource("/config.xml").toURI();
Path pathDoRecurso;
if(uriDoRecurso.toString().contains("!")) { //é de JAR
  //lógica para obter recurso do JAR...
} else {
  pathDoRecurso = Paths.get(uriDoRecurso);
}

Para obter a URI do recurso relativa à URI do JAR, devemos utilizar a ideia de sistema de arquivos do NIO.2, disponível a partir do Java 7. Um sistema de arquivos é abstraído através da classe abstrata java.nio.file.FileSystem. Por padrão, é utilizado o sistema de arquivos do sistema operacional. Porém, podemos utilizar um sistema de arquivos a partir de um JAR ou arquivo ZIP.

Os seguintes passos deverão ser tomados:

  • separar as partes da URI que são do JAR e do recurso dentro do JAR
  • criar uma URI que aponta para o JAR
  • criar um FileSystem a partir da URI do JAR
  • obter o Path do recurso relativo ao sistema de arquivos do JAR

Teremos, então, o código:

URI uriDoRecurso = MinhaClasse.class.getResource("/config.xml").toURI();
Path pathDoRecurso;
if(uriDoRecurso.toString().contains("!")) { //é de JAR
  String[] partesDaURI = uriDoRecurso.toString().split("!");
  URI uriDoJAR = URI.create(partesDaURI[0]);
  FileSystem fs = FileSystems.newFileSystem(uriDoJAR, new HashMap<String, String>());
  pathDoRecurso = fs.getPath(partesDaURI[1]);
} else {
  pathDoRecurso = Paths.get(uriDoRecurso);
}

Copiando arquivos de um diretório e seus sub-diretórios com Java 7+

Vamos dizer que temos um diretório chamado arquivos com o seguinte conteúdo:

.
└── arquivos
    ├── .bookignore
    ├── book.properties
    ├── imgs
        └── cover.jpg
    └── .gitignore

Queremos copiar todos os arquivos bem como o conteúdo do sub-diretório imgs para outro diretório chamado copia.

Fazer isso é fácil com os novos recursos do pacote java.nio disponíveis a partir do Java 7. Essas novidades foram definidas na JSR-203 e ficaram conhecidas como NIO.2.

Até o Java 1.3, havia somente o pacote java.io sem suporte adequado a abstrações de alto nível como channel, apenas com I/O bloqueante, suporte ruim a character encodings.

O Java 1.4 veio com o pacote java.nio que resolvia várias dessas limitações.

Com o NIO.2 do Java 7, foi simplificada a API do pacote java.nio, dado suporte a I/O assíncrono, criada uma abstração de sistema de arquivos, entre outras melhorias.

Para representar um diretório, devemos usar a classe Path, obtendo instâncias a partir da classe auxiliar Paths.

Path origem = Paths.get("./arquivos");
Path destino = Paths.get("./copia");

Para navegar no diretório de origem de forma a incluir sub-diretórios (ou seja, recursivamente) podemos usar o método estático walkFileTree da classe auxiliar Files. Precisamos passar um objeto que implementa a interface FileVisitor.

Files.walkFileTree(origem, new Copiador(origem, destino));

A classe Copiador define todos os métodos exigidos pela interface FileVisitor:

  • preVisitDirectory – chamado antes de visitar um novo sub-diretório
  • postVisitDirectory – chamado depois de visitar um sub-diretório e todos seus descendentes
  • visitFile – chamado ao visitar cada novo arquivo
  • visitFileFailed – chamando quando há falha em algum arquivo

Em cada método, devemos retornar um valor da enum FileVisitResult. Retornaremos FileVisitResult.CONTINUE, para continuar a navegação recursiva nos sub-diretórios.

class Copiador implements FileVisitor<Path> {
  private Path origem;
  private Path destino;
  public Copiador(Path origem, Path destino) {
    this.origem = origem;
    this.destino = destino;
  }
  @Override
  public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
    return FileVisitResult.CONTINUE;
  }
  @Override
  public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
    //copia arquivo para destino...
    return FileVisitResult.CONTINUE;
  }
  @Override
  public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
    throw exc;
  }
  @Override
  public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
    return FileVisitResult.CONTINUE;
  }
}

Ainda não preenchemos a lógica de copiar os arquivos, que deveria ficar no método visitFile.

Antes disso, podemos simplificar um pouco o código herdando da classe auxiliar SimpleFileVisitor e sobre-escrevendo apenas o método necessário.

class Copiador extends SimpleFileVisitor<Path> {
  private Path origem;
  private Path destino;
  public Copiador(Path origem, Path destino) {
    this.origem = origem;
    this.destino = destino;
  }
  @Override
  public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
    //copia arquivo para destino...
    return FileVisitResult.CONTINUE;
  }
}

No método visitFile, vamos pegar o caminho absoluto do arquivo que está sendo visitado transformando-o em um caminho relativo em relação ao diretório de origem.

Então, podemos obter o caminho do mesmo arquivo mas relativo ao destino.

Precisamos criar qualquer diretório “pai” intermediário necessário para o arquivo de destino, através do método estático createDirectories da classe auxiliar Files.

Aí, basta invocar o método estático copy de Files para efetuar a cópia.

No caso de algum diretório ou arquivo já existir, é lançada a exceção FileAlreadyExistsException, que vamos ignorar.

Teremos o código:

Path caminhoAbsoluto = file.toAbsolutePath();
Path caminhoRelativo = origem.relativize(caminhoAbsoluto);
Path arquivoDestino = destino.resolve(caminhoRelativo.toString());
try {
  Files.createDirectories(arquivoDestino);
  Files.copy(file, arquivoDestino);
} catch (FileAlreadyExistsException ex) {
  //se o diretorio ou arquivo ja existir, nao faz nada (nem sobreescreve)
}

Se quisermos sobre-escrever arquivos já existentes, basta passar o valor StandardCopyOption.REPLACE_EXISTING para o método copy.

Files.copy(file, target, StandardCopyOption.REPLACE_EXISTING);

Poderíamos utilizar uma classe anônima ao invés de ter uma classe que herda de SimpleFileVisitor. Juntando tudo, teríamos:

final Path origem = Paths.get("./src");
final Path destino = Paths.get("./copia");
Files.walkFileTree(origem, new SimpleFileVisitor<Path>() {
  @Override
  public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
    Path caminhoAbsoluto = file.toAbsolutePath();
    Path caminhoRelativo = origem.relativize(caminhoAbsoluto);
    Path arquivoDestino = destino.resolve(caminhoRelativo.toString());
    try {
      Files.createDirectories(arquivoDestino);
    } catch (FileAlreadyExistsException ex) {
      //se o diretorio ja existir, nao faz nada (nem sobreescreve)
    }
    Files.copy(file, arquivoDestino, StandardCopyOption.REPLACE_EXISTING);
    return FileVisitResult.CONTINUE;
  }
});

Fila de Mensagens Mortas

O que acontece quando há uma exceção em um recebedor registrado em uma fila?

Por exemplo:

@MessageDriven(activationConfig = {
  @ActivationConfigProperty(propertyName = "destinationLookup", propertyValue = "jms/FILA"),
  @ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Queue")
})
public class RecebedorMDB implements MessageListener {
  public void onMessage(Message msg) {
    throw new RuntimeException("Erro!");
  }
}

A classe acima é um Message Driven Bean (MDB) registrado em FILA que lança uma exceção não checada ao receber uma mensagem.

Se verificarmos a fila, a mensagem foi consumida. Só que aconteceu um erro!

De acordo com a especificação de mensageria do Java EE, o JMS (Java Message Service), um message listener que retorna uma RuntimeException é considerado um erro de programação e terá comportamento indefinido.

Aí entram detalhes da implementação: no HornetQ, o MOM (Message Oriented Middleware) do Wildfly 8.x.x, envia as mensagens que ocasionaram uma exceção para a fila DLQ. O nome vem da sigla para Dead Letter Queue, ou fila de mensagens mortas. É como se fosse um lugar para onde as mensagens vão morrer. O conceito também é conhecido como Dead Letter Channel ou Dead Message Queue.

Mas, na verdade, o recomendado é que os message listeners ou MDBs tratem todas as exceções. Uma boa solução é criarmos DLQs específicas para cada caso excepcional do processo de negócio. Ao detectarmos uma exceção, enviamos a mensagem para a DLQ correspondente.

O erro do criador do Ant

No livro Pragmatic Project Automation, publicado em 2004, há um trecho em que James Duncan Davidson (criador do Ant e do Tomcat) admite que foi um erro ter usado XML para descrever os passos do build do Ant.

Segue uma tradução livre:

O criador do Ant exorciza um de seus demônios
por James Duncan Davidson

A primeira versão do Ant não tinha esse monte de tags que você vê espalhadas pelos arquivos de build. Ao invés disso, era usado um arquivo de properties e a classe java.util.Properties para definir quais tasks deveriam ser executadas para um determinado target. Funcionou bem para pequenos projetos mas começou a entrar em colapso à medida que os projetos cresciam.

O motivo do colapso era a maneira que o Ant enxerga o mundo: um projeto é uma coleção de targets (alvos). Um target é uma coleção de tasks (tarefas). Cada task tem um conjunto de propriedades. É obviamente uma árvore hierárquica. Porém, arquivos .properties tem apenas mapeamentos chave=valor, nos quais essa estrututura de árvore não encaixa.

Eu queria um formato de arquivos hierárquico que capturasse a visão de mundo do Ant. Mas eu não queria criar meu próprio formato. Eu queria usar um formato padrão e, mais importante, eu não queria criar um parser. Eu queria reusar o trabalho dos outros. Eu queria o caminho mais fácil.

Na época, XML estava despontando no radar. A spec tinha sido finalizada, ainda que bem recentemente. SAX tinha se tornado um padrão de-facto, mas não tínhamos JAXP ainda. Eu estava convencido que XML era a next big thing depois do Java. Código portável e dados portáveis. Duas frases de efeito que ficam bem juntas.

E já que os dados no XML tem uma estrutura de árvore, parecia perfeito para o tipo de coisa que precisava ser expressado em um arquivo de build. Adicione o fato de que XML era um formato de texto editável manualmente, e parecia um casamento feito nos céus. E eu não precisava criar um parser. Trato feito.

Em retrospecto, XML provavelmente não foi a escolha correta como parecia. Eu tenho visto arquivos de build com centenas, e até milhares, de linhas e, nesses tamanhos, editar XML não é tão amigável como eu esperava. Além disso, quando você mistura XML e os mecanismos baseados em reflection do Ant que permitem estender e customizar tasks, você acaba com um ambiente que oferece o poder e a flexibilidade de uma linguagem de script — mas com a dor de cabeça de tentar expressar essa flexibilidade com tags no XML.

Minha intenção nunca foi que o formato de arquivo se transformasse em um linguagem de script. Afinal de contas, minha visão original do Ant era que teríamos a declaração de algumas propriedades que descreveriam o projeto e que as tasks escritas em Java fariam toda a lógica. Os atuais desenvolvedores do Ant em geral pensam o mesmo.

Mas quando eu fundi XML e reflection no Ant, fiz algo que é 70–80% de um ambiente de script. Só que não percebi na época. Negar que as pessoas iriam usar o XML do Ant como uma linguagem de script é equivalente a pedir para que finjam que açúcar não é doce.

Se eu soubesse o que sei agora, eu teria tentado usar uma linguagem de script de verdade, como JavaScript via o componente Rhino ou Python via Jython, com bindings para objetos Java que implementariam a funcionalidade expressadas nas tasks. Então, haveria uma forma natural de expressar lógica, e não estaríamos presos ao XML como formato que é muito ruim para o maneira como as pessoas querem usar a ferramenta.

Um processo de build é algo que comumente será customizado. Usar XML para definir os passos de um build foi um erro no Ant. E que foi herdado pelo Maven.

O poder de uma linguagem de script casa muito bem com a definição de um processo de build. E isso foi levado em conta em ferramentas mais novas como Gradle, que usa a linguagem Groovy.

Criando um jogo em Javascript [5/5] – Pontuação

Que tal finalizar nossa versão do jogo da galinha do Atari?

Fizemos o pano de fundo do jogo, no primeiro post.

A movimentação do personagem principal a partir do teclado foi feita no segundo post.

Alguns carros movimentados automaticamente foram inseridos no terceiro post.

No quarto post, o GAME OVER foi detectado ao ocorrer a colisão dos carros e nosso personagem principal.

Iniciando a pontuação

Vamos criar uma variável pontos para armazenar o número de pontos e também uma função desenhaPontos que, quando invocada, desenha os pontos no canto superior esquerdo da página.

var pontos = 0;
function desenhaPontos(){
    contexto.fillStyle = "black";
    contexto.font="12pt Monospace";
    contexto.fillText(pontos, 5, 20);
}

Na função do setTimeout, precisamos desenhar os pontos atuais. Devemos invocar a função que acabamos de criar depois de desenhar o fundo:

setInterval(function(){
    desenhaFundo();
    desenhaPontos();
    dilminha.desenhaImagem();
    carrinhoAmarelo.desenhaImagem();
    carrinhoAzul.desenhaImagem();
    carrinhoPolicia.desenhaImagem();

    //restante do código...
},50);

O resultado do código acima está disponível em:
http://a-dilminha.appspot.com/passo-a-passo/pontuacao/15-zerado.html

Pontuação zerada

Contando os pontos

No jogo do Atari, sempre que a galinha atravessa o lado de cima da rua ganhamos um ponto. Faremos o mesmo.

Quanto mais pra cima tiver o personagem principal, menor é o valor de y. Um valor menor ou igual a zero indica que o personagem atravessou a rua totalmente.

Vamos criar uma função passou que detecta se o personagem principal ultrapassou o limite superior da tela. Já que essa função não valerá pra os carros, vamos colocá-la diretamente como uma propriedade do objeto dilminha.

var dilminha = new Sprite("../../dilminha.png", 320, 400);
dilminha.passou = function(){
    if(this.y <= 0) {
        return true;
    }
    return false;
};

Sempre que houver alguma teclada pressionada no teclado, vamos verificar se a rua foi totalmente atravessada, invocando o método passou que acabamos de definir. Caso o personagem principal tenha ultrapassado o topo da tela, contaremos mais um ponto:

document.onkeydown = function(event) {
    //verificação de game over...
    //tratamento da tecla pressionada...
    if(dilminha.passou()){
        pontos++;
    }
}

O resultado do código acima está disponível em:
http://a-dilminha.appspot.com/passo-a-passo/pontuacao/16-pontos.html

Contagem de pontos

Bug na pontuação

Há um defeito na contagem dos pontos: se o personagem principal deslizar horizontalmente no topo da tela, são contados múltiplos pontos.

Veja:

Defeito na pontuação

Existem várias maneiras de resolver esse problema. Uma das mais simples é movermos o personagem principal lá pra baixo da tela assim que detectarmos que o topo da tela foi ultrapassado. Podemos implementar isso alterando o método passou:

dilminha.passou = function(){
    if(this.y <= 0) {
        this.y = canvas.height - this.altura;
        return true;
    }
    return false;
}

O resultado do código acima está disponível em:
http://a-dilminha.appspot.com/passo-a-passo/pontuacao/17-final.html

Defeito corrigido

Então é isso, pessoal

Poderíamos melhorar muito o joguinho: implementar uma colisão decente, salvar as pontuações em um servidor, fazer funcionar em dispositivos móveis…

Mas vamos parar por aqui! Já deu pra fazer algo razoável. Divertido, não?

Nosso código final ficou assim:

<html>
<body>
  <canvas id="canvas" width="640" height="480" style="border: solid 1px black; margin: 0px auto; display: block;"></canvas>
  <script>
    var gameOver = false;

    var canvas = document.getElementById("canvas");
    var contexto = canvas.getContext("2d");

    function desenhaFundo() {
      //preenche o fundo com cinza escuro
      contexto.fillStyle = "dimgray";
      contexto.fillRect(0, 0, canvas.width, canvas.height);

      //calcada superior
      contexto.fillStyle = "lightgray";
      contexto.fillRect(0, 0, canvas.width, 80);

      //calcada inferior
      contexto.fillStyle = "lightgray";
      contexto.fillRect(0, 380, canvas.width, 100);

      //faixas
      contexto.fillStyle = "white";
      for (var i = 0; i < 25; i++) {
        contexto.fillRect(i * 30 - 5, 185, 20, 4);
        contexto.fillRect(i * 30 - 5, 280, 20, 4);
      }
    }

    var pontos = 0;
    function desenhaPontos() {
      contexto.fillStyle = "black";
      contexto.font = "12pt Monospace";
      contexto.fillText(pontos, 5, 20);
    }

    function Sprite(caminhoDaImagem, xInicial, yInicial) {
      this.x = xInicial;
      this.y = yInicial;

      this.imagem = new Image();
      this.imagem.src = caminhoDaImagem;

      var that = this;
      this.imagem.onload = function () {
        that.largura = that.imagem.width;
        that.altura = that.imagem.height;
        that.desenhaImagem();
      }

      this.desenhaImagem = function () {
        contexto.drawImage(this.imagem, this.x, this.y, this.largura, this.altura);
        contexto.strokeStyle = "darkred";
        contexto.lineWidth = 0.2;
        contexto.strokeRect(this.x, this.y, this.largura, this.altura);
      }

      this.move = function (dx, dy) {
        this.x += dx;
        this.y += dy;

        //limites
        if (this.x > canvas.width) {
          this.x = -this.largura;
        } else if (this.x < -this.largura) {
          this.x = canvas.width;
        }
        if (this.y > canvas.height - this.altura + 5) {
          this.y -= dy;
        } else if (this.y <= -5) {
          this.y = canvas.height - this.altura;
        }
      }

      this.colidiu = function (outro) {
        var colidiuNoXTopo = outro.x >= this.x && outro.x <= (this.x + this.largura);
        var colidiuNoYTopo = outro.y >= this.y && outro.y <= (this.y + this.altura);
        var colidiuNoXBase = (outro.x + outro.largura) >= this.x && (outro.x + +outro.largura) <= (this.x + this.largura);
        var colidiuNoYBase = (outro.y + outro.altura) >= this.y && (outro.y + outro.altura) <= (this.y + this.altura);
        return (colidiuNoXTopo && colidiuNoYTopo) || (colidiuNoXBase && colidiuNoYBase);
      }

    }

    var dilminha = new Sprite("../../dilminha.png", 320, 400);
    dilminha.passou = function () {
      if (this.y <= 0) {
        this.y = canvas.height - this.altura;
        return true;
      }
      return false;
    }

    var carrinhoAmarelo = new Sprite("../../carrinho-amarelo.png", -10, 300);
    var carrinhoAzul = new Sprite("../../carrinho-azul.png", 560, 200);
    var carrinhoPolicia = new Sprite("../../carrinho-policia.png", 10, 100);

    document.onkeydown = function (event) {
      if (gameOver) {
        return;
      }

      switch (event.which) {
      case 37: //pra esquerda
        dilminha.move(-10, 0);
        break;
      case 38: //pra cima
        dilminha.move(0, -10);
        break;
      case 39: //pra direita
        dilminha.move(10, 0);
        break;
      case 40: //pra baixo
        dilminha.move(0, 10);
        break;
      }

      if (dilminha.passou()) {
        pontos++;
      }
    }

    setInterval(function () {
      desenhaFundo();
      desenhaPontos();

      dilminha.desenhaImagem();
      carrinhoAmarelo.desenhaImagem();
      carrinhoAzul.desenhaImagem();
      carrinhoPolicia.desenhaImagem();

      if (gameOver) {
        contexto.fillStyle = "red";
        contexto.font = "Bold 80px Sans";
        contexto.fillText("GAME OVER", canvas.width / 16, canvas.height / 2 + 20);
        return;
      }

      carrinhoAmarelo.move(7, 0);
      carrinhoAzul.move(-5, 0);
      carrinhoPolicia.move(10, 0);

      if (carrinhoAmarelo.colidiu(dilminha) || carrinhoAzul.colidiu(dilminha) || carrinhoPolicia.colidiu(dilminha)) {
        gameOver = true;
      }

    }, 50);
  </script>
</body>
</html>

O resultado do código acima está disponível em:
http://a-dilminha.appspot.com/index.html

Criando um jogo em Javascript [4/5] – Colisão

Vamos continuar nossa versão em Javascript do jogo da galinha do Atari.

No primeiro post, fizemos o pano de fundo do jogo.

Já no segundo post, fizemos o personagem principal, movendo-o a partir das setas do teclado e limitando até onde é possível ir.

Depois, no terceiro post, colocamos alguns carros que são movimentados automaticamente.

Agora, vamos detectar a colisão dos carros e nosso personagem principal, ocasionando um GAME OVER.

Detectando a colisão

Tanto o personagem principal como os três carrinhos são objetos criados a partir do construtor Sprite. Todos esses objetos tem as propriedades x, y, largura e altura.

Uma maneira de saber se houve colisão é considerar que as figuras são retângulos e verificar se houve intersecção.

Colisão de retângulos

Vamos definir um método colidiu no construtor Sprite que retorna true se houve intersecção entre os retângulos dos personagens:

function Sprite(caminhoDaImagem, xInicial, yInicial) {
    //restando do código...

    this.colidiu = function(outro){
        var colidiuNoXTopo = outro.x >= this.x && outro.x <= (this.x + this.largura);
        var colidiuNoYTopo = outro.y >= this.y && outro.y <= (this.y + this.altura);
        var colidiuNoXBase = (outro.x + outro.largura) >= this.x && (outro.x + + outro.largura) <= (this.x + this.largura);
        var colidiuNoYBase = (outro.y + outro.altura) >= this.y && (outro.y + outro.altura) <= (this.y + this.altura);
        return (colidiuNoXTopo && colidiuNoYTopo) || (colidiuNoXBase && colidiuNoYBase);
    }
}

Na função do setTimeout, logo após mover os carrinhos vamos verificar se algum colidiu com o personagem principal. Se houve colisão, vamos setar como verdadeira a variável gameOver.

Ainda na mesma função, depois de desenhar o fundo, o personagem principal e os carros, caso o jogo tenha terminado, vamos escrever com uma fonte vermelha bem grande o texto GAME OVER. Para isso, devemos utilizar a função fillText do contexto 2D. Também colocaremos um return antecipado, impedindo que os carros movam-se se o jogo tiver terminado.

Teremos então:

setInterval(function(){
    desenhaFundo();
    dilminha.desenhaImagem();
    carrinhoAmarelo.desenhaImagem();
    carrinhoAzul.desenhaImagem();
    carrinhoPolicia.desenhaImagem();

    if(gameOver){
        contexto.fillStyle = "red";
        contexto.font="Bold 80px Sans";
        contexto.fillText("GAME OVER", canvas.width/16, canvas.height/2+20);
        return;
    }

    carrinhoAmarelo.move(7, 0);
    carrinhoAzul.move(-5, 0);
    carrinhoPolicia.move(10, 0);

    if(carrinhoAmarelo.colidiu(dilminha)
        || carrinhoAzul.colidiu(dilminha)
        || carrinhoPolicia.colidiu(dilminha)){
        gameOver = true;
    }
},50);

Outra coisa que precisamos fazer é impedir que o personagem principal seja movido pelo teclado depois do GAME OVER. Para isso, faremos um retorno antecipado caso a variável gameOver esteja setada.

document.onkeydown = function(event) {
    if(gameOver){
        return;
    }
    //restante do código...
}

O resultado do código acima está disponível em:
http://a-dilminha.appspot.com/passo-a-passo/colidindo/13-colisao.html

Colisão do carro amarelo com o personagem

Desenhando um retângulo nas bordas

Perceba na imagem acima que há uma certa distância na colisão entre o carro amarelo e o personagem principal.

Será que o cálculo do método colidiu está errado? Na verdade, está OK. O problema é que fica difícil de ver onde começa e onde termina o retângulo da imagem.

Por isso, vamos alterar o método desenhaImagem para que seja desenhado uma borda em cada uma das imagens. Assim, teremos um feeback visual do porquê dos objetos estarem colidindo uns com os outros.

O ideal (mas complexo) seria fazer algum algoritmo de colisão mais sofisticado.

O retângulo pode ser desenhado usando a função strokeRect do contexto 2D.

this.desenhaImagem = function(){
    contexto.drawImage(this.imagem, this.x, this.y, this.largura, this.altura);
    contexto.strokeStyle="darkred";
    contexto.lineWidth = 0.2;
    contexto.strokeRect(this.x, this.y, this.largura, this.altura);
}

O resultado do código acima está disponível em:
http://a-dilminha.appspot.com/passo-a-passo/colidindo/14-box.html

Retângulos nas bordas

Tem mais

Implementamos a detecção da colisão entre os carros e o personagem principal. Caso haja colisão, acontece o GAME OVER.

Falta o mais importante do jogo: a pontuação. Será o assunto do próximo post!

Invalidando o cache do Docker

O Docker é uma tecnologia fantástica. Através de recursos do kernel do Linux, a ferramenta permite executar o código da sua aplicação em “máquinas virtuais” (na verdade, containers) muito leves.

Uma coisa chata é que o Docker funciona nativamente apenas em distribuições Linux. Pra usar no Mac OS X ou no Windows, você precisa de uma máquina virtual VirtualBox.

Receita

Com o Docker, você pode definir uma “receita” em um arquivo chamado Dockerfile. Através de comandos pré-definidos, você fornece um passo-a-passo que é usado para criar templates de máquinas, conhecidos como imagens. As imagens são usadas para criar os containers que realmente executam aplicações. Um exemplo de Dockerfile seria:

FROM ubuntu:trusty

RUN apt-get install -y --no-install-recommends \
	curl \
	git

#instalando nodejs 0.12.4 (com npm 2.10.1 incluso)
RUN curl -sSLO "http://nodejs.org/dist/v0.12.4/node-v0.12.4-linux-x64.tar.gz" \
	&& tar -xzf "node-v0.12.4-linux-x64.tar.gz" -C /usr/local --strip-components=1 \
	&& rm "node-v0.12.4-linux-x64.tar.gz" 

RUN npm install -g alexandreaquiles/marechal-random

CMD marechal-random

O comando FROM permite o uso de uma imagem pré-definida. Já o RUN permite que algo seja executado na nova imagem que está sendo criada. O CMD define o comando que deve ser executado por padrão nos containers criados a partir dessa imagem.

No Dockerfile acima:

  • criamos uma imagem baseada no Ubuntu 14.04 (trusty);
  • instalamos as ferramentas curl e git;
  • instalamos o Node JS;
  • através do npm, instalamos o módulo Node JS alexandreaquiles/marechal-random globalmente, permitindo que seja executado em linha de comando. O código é obtido a partir do repositório github.com/alexandreaquiles/marechal-random (e não do npm registry);
  • configuramos a execução do comando marechal-random por padrão, que invoca nosso módulo Node JS

Executando

O módulo Node JS marechal-random é uma ferramenta bem simples de linha de comando que, quando executada, mostra um número randômico maior que 0 e menor que 1. O código do arquivo index.js é o seguinte:

#!/usr/bin/env node
console.log(Math.random());

Por meio do comando docker build, podemos criar uma imagem chamada maquina a partir do Dockerfile:

docker build -t maquina pasta-do-Dockerfile/

Será mostrado algo como:

Sending build context to Docker daemon 2.048 kB
Step 1 : FROM ubuntu:trusty
 ---> 6d4946999d4f
Step 2 : RUN apt-get install -y --no-install-recommends 	curl 	git
 ---> Running in 512f2fb0af33
Reading package lists...
Building dependency tree...
Reading state information...
The following extra packages will be installed:
  git-man libasn1-8-heimdal libcurl3 libcurl3-gnutls liberror-perl ...
Recommended packages:
  patch rsync ssh-client ca-certificates krb5-locales libsasl2-modules
The following NEW packages will be installed:
  curl git git-man libasn1-8-heimdal libcurl3 libcurl3-gnutls liberror-perl ...
0 upgraded, 25 newly installed, 0 to remove and 0 not upgraded.
Need to get 5367 kB of archives.
After this operation, 29.3 MB of additional disk space will be used.
...
 ---> 78b2ceb410cc
Removing intermediate container 512f2fb0af33
Step 3 : RUN curl -sSLO "http://nodejs.org/dist/v0.12.4/node-v0.12.4-linux-x64.tar.gz" 	&& tar -xzf "node-v0.12.4-linux-x64.tar.gz" -C /usr/local --strip-components=1 	&& rm "node-v0.12.4-linux-x64.tar.gz"
 ---> Running in 8dc493ab0aeb
 ---> 41f993547202
Removing intermediate container 8dc493ab0aeb
Step 4 : RUN npm install -g alexandreaquiles/marechal-random
 ---> Running in e366e4914731
/usr/local/bin/marechal-random -> /usr/local/lib/node_modules/marechal-random/index.js
marechal-random@1.0.0 /usr/local/lib/node_modules/marechal-random
 ---> 748021adbd43
Removing intermediate container e366e4914731
Step 5 : CMD marechal-random
 ---> Running in 99d8f0df0e79
 ---> 9974d8f6a06b
Removing intermediate container 99d8f0df0e79
Successfully built 9974d8f6a06b

Então, através do docker run, podemos executar o comando padrão:

docker run --rm  maquina

É criado um container a partir da imagem maquina. A opção --rm faz com que esse container seja removido logo após o término da execução, economizando espaço em disco.

O resultado será algum número randômico:

0.6202559822704643

Se executarmos novamente, teremos outros resultado:

0.13229440338909626

Alterações

Vamos alterar esse módulo, para que seja impressa a mensagem “Random” antes do número:

#!/usr/bin/env node
console.log('Random: '+Math.random());

Depois de feito commit e push para o repositório do GitHub, vamos regerar a imagem para que o módulo marechal-random seja atualizado:

docker build -t maquina pasta-do-Dockerfile/

O resultado seria:

Sending build context to Docker daemon 2.048 kB
Step 1 : FROM ubuntu:trusty
 ---> 6d4946999d4f
Step 2 : RUN apt-get install -y --no-install-recommends 	curl 	git
 ---> Using cache
 ---> 78b2ceb410cc
Step 3 : RUN curl -sSLO "http://nodejs.org/dist/v0.12.4/node-v0.12.4-linux-x64.tar.gz" 	&& tar -xzf "node-v0.12.4-linux-x64.tar.gz" -C /usr/local --strip-components=1 	&& rm "node-v0.12.4-linux-x64.tar.gz"
 ---> Using cache
 ---> 41f993547202
Step 4 : RUN npm install -g alexandreaquiles/marechal-random
 ---> Using cache
 ---> 748021adbd43
Step 5 : CMD marechal-random
 ---> Using cache
 ---> 9974d8f6a06b
Successfully built 9974d8f6a06b

Perceba que todos os comandos tem a mensagem Using cache, incluindo a instalação do módulo marechal-random (Step 4).

Vamos, então, executar novamente nosso comando:

docker run --rm  maquina

Teremos:

0.2565556331537664

A mensagem “Random” não apareceu na frente do número!

A raiz do problema está na criação da imagem maquina a partir do Dockerfile: se exatamente o mesmo comando for executado, é utilizado o cache pré-existente. E o que é comparado é o texto do comando! Como RUN npm install -g alexandreaquiles/marechal-random já tinha sido executado, foi utilizado o cache 748021adbd43.

Invalidação de cache

O que fazer então para que, toda vez que mudarmos o código de marechal-random no GitHub, seja criada uma nova imagem?

Precisamos fazer com que o cache seja invalidado.

O comando COPY pode ser usado no Dockerfile para copiar um arquivo para dentro da imagem que está sendo criada. Se o conteúdo do arquivo ou algum metadado for modificado, o cache é invalidado no COPY e nos comandos posteriores.

Vamos passar a instalação do módulo marechal-random para um arquivo chamado instala.sh:

#!/bin/bash
npm install -g alexandreaquiles/marechal-random

É importante que o arquivo instala.sh tenha permissão de execução.

Depois disso, vamos modificar o Dockerfile para que o arquivo instala.sh seja copiado para a imagem:

FROM ubuntu:trusty

RUN apt-get install -y --no-install-recommends \
	curl \
	git

#instalando nodejs 0.12.4 (com npm 2.10.1 incluso)
RUN curl -sSLO "http://nodejs.org/dist/v0.12.4/node-v0.12.4-linux-x64.tar.gz" \
	&& tar -xzf "node-v0.12.4-linux-x64.tar.gz" -C /usr/local --strip-components=1 \
	&& rm "node-v0.12.4-linux-x64.tar.gz" 

COPY instala.sh /modulos/
RUN /modulos/instala.sh

CMD marechal-random

Devemos regerar a imagem maquina:

docker build -t maquina pasta-do-Dockerfile/

Teremos a seguinte saída:

Sending build context to Docker daemon 2.048 kB
Step 1 : FROM ubuntu:trusty
 ---> 6d4946999d4f
Step 2 : RUN apt-get install -y --no-install-recommends 	curl 	git
 ---> Using cache
 ---> 78b2ceb410cc
Step 3 : RUN curl -sSLO "http://nodejs.org/dist/v0.12.4/node-v0.12.4-linux-x64.tar.gz" 	&& tar -xzf "node-v0.12.4-linux-x64.tar.gz" -C /usr/local --strip-components=1 	&& rm "node-v0.12.4-linux-x64.tar.gz"
 ---> Using cache
 ---> 41f993547202
Step 4 : COPY instala.sh /modulos/
 ---> d3bbc70bc9ac
Removing intermediate container b19aa7ff986b
Step 5 : RUN /modulos/instala.sh
 ---> Running in 37fc84fbcb65
/usr/local/bin/marechal-random -> /usr/local/lib/node_modules/marechal-random/index.js
marechal-random@1.0.0 /usr/local/lib/node_modules/marechal-random
 ---> 686a674f0e1f
Removing intermediate container 37fc84fbcb65
Step 6 : CMD marechal-random
 ---> Running in 11a58e68a5d6
 ---> cbface0310ed
Removing intermediate container 11a58e68a5d6
Successfully built cbface0310ed

Perceba que, para os três primeiros comandos, foram utilizados os caches. A partir do COPY (Step 4), não foi usado o cache. No RUN (Step 5), foi feita a instalação do módulo marechal-random.

Mas não foi usado o cache desses comandos porque modificamos o conteúdo do Dockerfile. Para testar se a invalidação está funcionando, precisamos regerar a imagem:

docker build -t maquina pasta-do-Dockerfile/

Teremos:

Sending build context to Docker daemon 3.072 kB
Step 1 : FROM ubuntu:trusty
 ---> 6d4946999d4f
Step 2 : RUN apt-get install -y --no-install-recommends 	curl 	git
 ---> Using cache
 ---> ce97fca9b976
Step 3 : RUN curl -sSLO "http://nodejs.org/dist/v0.12.4/node-v0.12.4-linux-x64.tar.gz" 	&& tar -xzf "node-v0.12.4-linux-x64.tar.gz" -C /usr/local --strip-components=1 	&& rm "node-v0.12.4-linux-x64.tar.gz"
 ---> Using cache
 ---> bfef268a14e4
Step 4 : COPY instala.sh /modulos/
 ---> Using cache
 ---> d3bbc70bc9ac
Step 5 : RUN /modulos/instala.sh
 ---> Using cache
 ---> 686a674f0e1f
Step 6 : CMD marechal-random
 ---> Using cache
 ---> cbface0310ed
Successfully built cbface0310ed

Ih! Não funcionou! Foi usado o cache para todos os comandos!

Acontece que, para invalidar o cache do COPY, precisamos mudar o conteúdo ou metadados do arquivo instala.sh.

Até a versão 1.8 do Docker, poderíamos modificar o último acesso do arquivo com um touch. Porém, da versão 1.8 em diante, o último acesso e última modificação de arquivo são desconsiderados.

Para mudar o conteúdo do instala.sh sem mudar seu comportamento, podemos adicionar um comentário com a data atual na última linha do arquivo:

echo "#$(date)" >> instala.sh

Então, vamos construir novamente a imagem maquina:

docker build -t maquina pasta-do-Dockerfile/

Veremos como resposta:

Sending build context to Docker daemon 3.072 kB
Step 1 : FROM ubuntu:trusty
 ---> 6d4946999d4f
Step 2 : RUN apt-get install -y --no-install-recommends 	curl 	git
 ---> Using cache
 ---> ce97fca9b976
Step 3 : RUN curl -sSLO "http://nodejs.org/dist/v0.12.4/node-v0.12.4-linux-x64.tar.gz" 	&& tar -xzf "node-v0.12.4-linux-x64.tar.gz" -C /usr/local --strip-components=1 	&& rm "node-v0.12.4-linux-x64.tar.gz"
 ---> Using cache
 ---> bfef268a14e4
Step 4 : COPY instala.sh /modulos/
 ---> fdcbc296ca90
Removing intermediate container c8ddba926b63
Step 5 : RUN /modulos/instala.sh
 ---> Running in 84bae6632f68
/usr/local/bin/marechal-random -> /usr/local/lib/node_modules/marechal-random/index.js
marechal-random@1.0.0 /usr/local/lib/node_modules/marechal-random
 ---> ec2b0f560f1c
Removing intermediate container 84bae6632f68
Step 6 : CMD marechal-random
 ---> Running in 54d91a990478
 ---> 9c4aefc0ab6b
Removing intermediate container 54d91a990478
Successfully built 9c4aefc0ab6b

Uma boa prática é colocar os comandos que invalidarão o cache por último no seu Dockerfile.