Tag Archive | gamedev

Representação do mundo de jogos Bomberman-like

Existem aqueles problemas não-imediatos como desempenho, elegância ou outros que apenas ficam no caminho para a primeira solução funcional. Há aqueles que condenam a prática de preocupação prévia e insistem em resultados imediatos. Estou trabalhando no desenvolvimento de um jogo similar ao Bomberman e precisei desenvolver uma solução para representar o mapa onde o mundo do jogo é executado. E, é claro, acabei tendo o luxo de escolher a abordagem de desenvolvimento onde a preocupação com problemas não-imediatos ocorre mais cedo que a entrega do produto.

Introdução

Para que o problema não fique muito abstrato, considere a imagem abaixo, retirada do jogo BlowThemAll.

shot-2015-05-09_14-08-52

O mapa do jogo possui uma implementação similar ao conceito de uma matriz, onde os elementos do mapa possuem uma posição <x, y> tal que essa posição não pode ultrapassar os limites da matriz e os índices x e y são números discretos, naturais. Cada posição no mapa é um tile e cada tile pode conter vários elementos.

O mapa contém elementos imóveis, tais como blocos a serem explodidos, e elementos móveis, tais como bombas e avatares. Avatar é um personagem que representa a conexão do jogador com o mundo virtual de BlowThemAll.

Há ainda blocos eternos, que não podem ser explodidos.

No jogo original, Bomberman, uma bomba explode e a explosão tem o alcance baseado em tiles. Itens aparecem somente em tiles específicos. Há muitas observações que sugerem fortemente o uso de uma matriz como uma representação eficiente para o jogo.

Problema

Jogando-se Bomberman é possível observar que um avatar pode estar entre dois tiles, então você não pode representá-los como um elemento em uma sequência que faz parte de algum tile, pois um avatar pode estar entre dois tiles. Logo, o avatar precisa ser independente dessa matriz e não pode ter sua posição representada por números discretos.

Abordagens iniciais

A primeira abordagem para o problema foi utilizar uma lista separada para esses elementos do mapa que podem assumir posições entre dois tiles e armazenar suas respectivas posições usando valores do tipo double (ou parecidos).

Entretanto, eu não me sinto confortável usando valores do tipo double em um jogo em rede. Primeiro, porque o comportamento de doubles não é trivial, e segundo, porque é difícil garantir reprodutibilidade/previsibilidade usando doubles, então  eu não quero lidar com o problema desses números sendo utilizados numa parte principal de um jogo em rede. Lembrando que o double que você está usando pode nem ser o IEEE floating point. É claro que eu iria ler o “what every computer scientist should know about floating point numbers” caso o projeto precisasse, mas eu queria, ao menos, considerar uma abordagem alternativa.

A minha ideia foi usar números discretos para as posições dos elementos não-fixos no mapa, e fazer numa escala diferente (se o mapa era 13×11 tiles, os elementos não-fixos estariam em uma matriz virtual 130×110) para permitir um valor mais granular (que emularia o fato do avatar poder estar entre dois tiles). Essa abordagem não me contentou, pois ela levantava outras perguntas (ex.: quão granular devo permitir a posição para permitir uma experiência de jogo agradável?), não era elegante e tirava a simplicidade que eu tinha na solução inicial, onde, era barata, a operação de encontrar quais os avatares eram atingidos por uma explosão.

Pensando nas restrições

Eu voltei ao problema inicial para pensar nas restrições que ele apresentava. A depender de suas restrições, o algoritmo poderia resolver um problema menor e mais simples. Foi essa a abordagem que me permitiu desenvolver a libdepixelize com menos código e mais rápido, e é isso que decidi também fazer no BlowThemAll, entender melhor o problema.

Daí, percebi algo enquanto jogava bomberman. O avatar pode sim, estar entre duas posições, mas caso ele esteja entre duas posições, ainda há um “tile principal” ao qual ele pertence. Isto é, ele só é atingido por uma explosão caso apenas um dos tiles, o “tile principal”, seja incendiado. Além disso, há a restrição de que ele só pode estar entre dois tiles, não entre quatro tiles. Todas essas informações me inspiraram o suficiente para pensar numa solução alternativa.

Solução final

A ideia-chave da solução que acabei adotando é armazenar os elementos na “matriz”, como estava inicialmente planejado, porém adicionar um atributo “filled”, que representava o quanto  daquele tile o elemento ocupa, e um atributo direção, que representa qual o próximo tile a ser ocupado, quando o elemento atual chegar no limite de desocupação do tile atual.

Conceitualmente, 1 representa o estado onde o elemento está ocupando somente aquele tile, 0 representa o estado onde ele não está ocupando nada daquele tile e temos também todos os valores intermediários (como ocorre em contas de probabilidade). Entretanto, queremos usar valores discretos, então usamos inteiros simples e usamos seus limites (0 e 255) para representar os limites conceituais (0 e 1).

Um elemento é transferido para o próximo tile, quando, conceitualmente, ele desocupa mais da metade do tile atual. Se mapearmos o comportamento conceitual para o comportamento em código, então haverá um desperdício de bits, pois o valor de filled nunca seria inferior a 127. Assim sendo, o valor 255 continua representando o conceitual 1, mas o valor 0 representa o conceitual 1/2.

E a solução ficou bem legal até, mas eu ainda queria mais. Uma granularidade de 256 valores possíveis parecia um desperdício para mim e eu queria usá-los para mais informações. Foi então que eu decidi separar preenchimento/ocupação/”filled” por dimensão. “dir” se transformou nos atributos “is_up” e “is_left”, enquanto “filled” se transformou nos atributos “y_filled” e “x_filled”. Usando campos de bits, consegui armazenar toda essa informação em um único byte.

Os atributos “is_up” e “is_left” podem lembrar o bit de sinal da representação de inteiros por complemento de 1, que desperdiça um valor possível, porém eu quero preservar a simetria, então esse desperdício é aceitável.

Essa separação por dimensão, entretanto, faz com que o objeto sempre assuma uma direção para cada dimensão. Se antes as opções de direção eram norte, sul, leste e oeste, agora as direções são nordeste, noroeste, sudeste, sudoeste. Essa característica não é um problema, mas é preciso manter ela em mente quando for implementar funcionalidades que dependem da direção do personagem (socos, por exemplo).

Outra consequência da separação é que o jogo volta a apresentar a possibilidade inicial onde um personagem pode assumir uma posição de estar entre quatro tiles. Essa é uma característica positiva, pois impõe menos restrições e dá mais liberdade artística aos autores de mapas. Essa possibilidade também está presente no jogo Bomberman Tournament e eu não quero fazer menos que os competidores.

E, assim, eu fiquei satisfeito, e surgiu o código do BlowThemAll para tratar mapas. É claro que eu não acho que usaria essa solução para um jogo estilo “The Legend of Zelda: A Link to the Past“, mas eu acho que ficou bem adequada a um jogo ao estilo de Bomberman. Pode ser que você ache interessante pesquisar também sobre Quadtree Collision detection.

Programação em tempo real

Caso não fosse um jogo em rede e que possui algumas características que eu não detalhei, eu estaria também mantendo a preocupação de fazer uma solução para programação em tempo real, afinal, é um jogo e queremos previsibilidade nos tempos de respostas máximos. Sem essa preocupação, a única modificação que pensei para melhorar a previsibilidade nos tempos de resposta seria usar a função vector::reserve para pré-alocar um pouco de memória para a lista de elementos em cada tile.

Final

E para acabar o post, deixo um vídeo de alguns segundos demonstrando o resultado:

Game Jam durante o final de semana

Esse final de semana participei de um evento de game jam. Sinceramente, não pesquisei sobre esse tipo de evento e posso estar sendo infiel aos valores-raízes e desvirtuando o mesmo, pois tudo que sei é informação de segunda-mão, mas, mesmo assim, tive uma experiência positiva com a minha visão do que é um game jam. Ao final do processo, acabei conhecendo como é um game jam e com uma nova sugestão de evento para quem gosta de programar.

O que é?

Game Jam é um evento onde pessoas se reúnem num mesmo local para fazer um jogo do zero. Elas se reúnem, formam times e criam um jogo. O desenvolvimento do jogo deveria durar três dias e, ao final do processo, deveria haver um produto “mostrável”.

Por que?

É divertido, e você se desapega daquelas preocupações de código elegante e reaproveitável que faz verificações de segurança para terminar algo rápido. Ter algo que você fez e está funcionando é algo legal. E o ambiente onde há outras pessoas programando, buscando o mesmo objetivo que você, estimula você a ficar determinado em sua tarefa. E também é uma desculpa para trabalhar com tecnologias novas e melhorar suas habilidades como programador.

Meu game jam

Esse game jam que participei foi uma coisa pequena que organizei na minha casa e chamei uns amigos. Foram três times ao total (sendo que dois times, incluindo o meu, eram formados por apenas um membro).

photo420830555499767723

Time azul

O time azul desenvolveu um jogo estilo Beat ‘em up escrito em Python 3 e pygame.

Time vermelho

IMG_20150502_200110

Captura de tela de 2015-05-02 20_06_55

O time vermelho desenvolveu um jogo de asteroides que também foi escrito em pygame, mas utilizando Python 2.

Time laranja

photo284176472941373377

O meu time, que no caso sou só eu mesmo, fez um jogo estilo bomberman via rede, escrito em C++11, Qt5 e Tufão 1.x.

Dicas

Lembre-se de se divertir, é o mais importante. Eu podia ter formado time com a outra pessoa que também ficou sem time, mas nós tínhamos expectativas diferentes e ambos estaríamos frustrados caso tivéssemos formado um time. No final, acho que todo mundo achou legal. Se divertir é importante.

Veredito

A experiência que tive com o evento foi positiva e é um evento que eu recomendo a todo mundo que gosta de programar. Vale a pena.

Sobre os jogos, creio que os participantes vão continuar trabalhando neles por um pouco mais de tempo, pelo menos, para ter algo polido e tal. O meu jogo, pelo menos, tem código ainda não público e você precisa de um manual de instruções para rodar o jogo sem esbarrar nos glitches e bugs.

Para o próximo, planejo fazer algo usando Rust, SDL2 e Chipmunk, três tecnologias com as quais não tenho experiência significativa e, no máximo, apenas uma ligeira familiaridade.

Arquivos portáveis, protocolos portáveis… em C!

Um hábito que me faz desprezar imediatamente TODAS as habilidades que um programador afirme ter, é ele fazer um formato de arquivo ou protocolo binário que não é portável, então resolvi fazer esse artigo para ajudar a diminuir essa prática, expondo os seus perigos.

Mas antes, lembro que na época que eu usava Flash Player, eu era um usuário burro e aceitava cegamente a desculpa das versões de diferentes sistemas operacionais serem dessincronizadas (sistema operacional X recebe versão nova antes de Y e Z, em vez de lançamento simultâneo). Isso é um ato que eu até consigo entender tendo em mente os objetivos da empresa, mas outro lugar onde a dessincronização de versões ocorria era em arquiteturas diferentes (32bit e 64bit). E o único fator para justificar isso é pura incompetência dos programadores, que ganharam para sempre o meu desprezo. Dito isso, não acompanho ou uso mais o Flash Player e não sei se esse problema já foi corrigido.

Para não comentarem que sou extremista e só exponho os erros técnicos de softwares proprietários, aqui e aqui você vai encontrar um erro que pode ocasionar o mesmo problema, no futuro, mas é completamente ignorado, no projeto Enlightenment, que é software livre. Aliás, eu nem posso afirmar que expus erros técnicos do software Flash Player, pois é assim que funciona com software proprietário, você simplesmente não sabe.

Citando, para motivos de reflexão, antes do começar a parte séria do texto, a frase de alguns padrões da IETF, em tradução livre: “seja liberal no que você aceita, mas conservador no que você produz”.

Fluxos de bytes binários

O computador trabalha usando binários, mas esse fato é abstraído na linguagem de programação e esquecemos isso enquanto estamos focando no problema que queremos resolver de verdade, em vez de ficarmos nos preocupando com o opcode da instrução da arquitetura do processador que é necessária para implementar várias camadas de algoritmos até chegar no problema que realmente queremos resolver. Entretanto, quando é hora de fazer o programa se comunicar com o mundo externo, através de arquivos ou sockets, é hora de nos preocuparmos com a representação interna das estruturas do programa, pois nesse momento elas importam.

<stdint.h> e <inttypes.h>

Começando pelo mais simples, C não exige/garante que os tipos short, int, long, long long tenham a mesma quantidade de bits para todas implementações, ou mesmo que o tipo char tenha sinal. Isso significa que se você fizer um programa que leia um long de um arquivo ou da rede, você está errado. Quando você compilar o seu programa em uma implementação que usa um long de 32 bits, ele não se comunicará adequadamente com o mesmo programa compilado em uma implementação que usa um long de 64 bits, pois a estrutura de fluxo de bytes que ele gera não é definida e varia de implementação para implementação.

A solução para esse problema é você usar tipos para os quais existe a garantia de tamanho específico, independente de implementação. Se um compilador não cumprir com esse requisito, ele não é um compilador da linguagem C, pois ele não cumpre com a especificação da linguagem. Esses inteiros estão disponíveis nos cabeçalhos <stdint.h> e <inttypes.h>. O cabeçalho <stdint.h> possui todos os nomes necessário para declarar inteiros de tamanho fixo, enquanto o <inttypes.h>, além dos nomes introduzidos pelo <stdint.h>, introduz nomes para ajudar a trabalhar com printf, scanf e conversões entre strings e inteiros de tamanho fixo.

Mais uma coisa a aprender é que o padrão mais comum para representar números negativos usando binários é o complemento de 2, mas a especificação da linguagem não exige/garante o padrão por trás do mesmo e, inclusive, define integer overflow como undefined behaviour. Mas nada tema, pois o padrão exige que os inteiros com sinal declarados no cabeçalho <stdint.h> sejam armazenados usando complemento de dois. Então se você está seguindo as soluções até aqui, você já se livrou do problema.

Uma última nota que tenho a adicionar é: só use inteiros de tamanho fixo quando eles forem necessários.

Ordem dos bytes

Outro problema que atinge a portabilidade do fluxo de bytes é que não há garantias em relação a ordem dos bytes que compõem um mesmo inteiro. Isto é, em 16 bits, é legal representar 16 bits como 0000’0001’0000’0000 ou 0000’0000’0000’0001. As duas ordenações que costumamos encontrar são Little Endian e Big Endian (também conhecido com Network Byte Order).

Não há solução embutida na especificação da linguagem C para esse problema, então você vai ter que implementar suas funções ou usar alguma biblioteca externa como essa ou essa.

EBML/Matroska

Um formato que eu quero citar brevemente, pois possui uma característica que eu acho bem legal, é o EBML. O EBML é binário e possui uma representação eficiente, sendo usado, por exemplo, no formato de arquivo Matroska. Mas o EBML é também extensível, o que me dá confiança para acreditar que não seria surpresa continuarmos a utilizar esse formato por, sei lá, mais 20 anos.

Imagine que lhe seja dada a tarefa de criar uma representação binária para datas. Você decide especificar a quantidade de bits necessários para dias (que variam entre 1 e 31) e meses, mas chega então no problema de especificar a representação para armazenar o ano. Você sente a vontade de especificar um tamanho fixo grande o suficiente para que o problema não seja exposto enquanto você estiver vivo, mas algo dentro de você grita para que você lute e acabe com o problema. Esse algo dentro de você, gritando para que você resolva o problema, se chama competência. Você decide usar caracteres ASCII no topo do formato binário, para usar texto e ter uma quantidade infinita de caracteres, que pode ser usada para representar qualquer data, mas surge a questão de como definir o tamanho para armazenar essa cadeia de caracteres. Se você especificar um inteiro de 32 bits ou qualquer outro tamanho no começo da sequência, a representação continuará limitada.

A linguagem C define cadeias de caracteres de tamanho arbitrários (na verdade, ainda limitados pelo intervalo enumerável dos ponteiros) através de um delimitador no final da sequência. A mesma solução de usar um delimitador pode ser trazida ao universo dos inteiros binários e, de fato, é usada no formato EBML. Vale ressaltar que o EBML não sofre o gargalo e ineficiência de usar caracteres ASCII convertidos lexicalmente (no sentido do lexical_cast existente na Boost) para inteiros em sua representação.

Baseado em texto puro (legível por humanos)

Não, seus problemas não acabam com você indo para texto puro! Mas nada tema, pois você usa uma linguagem que requer a presença de acentos, está usando um locale diferente do padrão e já possui vantagem sobre outros programadores, detectando mais cedo qualquer quebra de internacionalização!

Se o fluxo de bytes que você está definindo é baseado em texto puro, você deve responder algumas perguntas como “o conjunto de caracteres ASCII é suficiente?”, “como adicionar estrutura de uma forma extensível?” e “dados binários serão suficientes?”.

O primeiro problema a resolver é que, caso abra um arquivo em modo de texto, em C, o final de linha será interceptado e convertido, a depender do sistema operacional (“\n” no sistema pai, o Unix, “\r\n” no Windows e “\r” no sistema da empresa que gosta de quebrar padrões para eventualmente causar dependência). Então, ou você escolhe e usa apenas uma dessas opções (como acontece com o HTTP), ou se prepara para tratar todas elas (como acontece com o gcc e editores de texto decentes). Mas um comportamento deve ser definido (assim como suas consequências refletidas) e a implementação deve cumprir com tal.

Se eventualmente for necessário adicionar suporte a dados binários, pesquise pelos padrões base64 e base91 e faça preparativos para que não haja conflitos de caracteres especiais sendo utilizados em áreas diferentes das planejadas.

Se o conjunto de caracteres ASCII não for suficiente, use Unicode. Vou apenas lhe informar para começar por aqui.

Problemas em XML e arquivos estruturados em geral

O XML é um formato de arquivo que carrega a palavra extensibilidade em seu nome (eXtensible Markup Language) e ele é bastante usado em alguns locais onde manter interoperabilidade é um requisito importante. Os problemas que podem aparecer aqui não são de baixo nível, específicos de arquitetura de computadores ou da linguagem C, mas já que comecei a comentar sobre interoperabilidade, decidi estender o texto e fugir do escopo inicial só um pouco.

Um problema que costuma afetar o XML e formatos estruturados como um todo, é que algumas implementações, não-conformantes, costumam usar a estratégia ingênua de mapear o arquivo para suas próprias estruturas e trabalhar daí, em vez de manipular o arquivo original. Acontece que normalmente essa estratégia ingênua irá descartar todos os atributos, tags, elementos e por aí vai do arquivo original, que não forem reconhecidas pela implementação. Isso aconteceu com algumas implementações do ODF e houve até a teoria conspiratória que a infratora tentava minar o padrão.

Mesmo que você argumente em optar pela abordagem ingênua, por sua implementação suportar todos os nomes definidos na especificação, você deve ter em mente que formatos evoluem e extensões são adicionadas no topo do formato original (isso acontece com o OpenGL, com o XML SVG do Inkscape e em tantos outros por aí).

Uma abordagem mais legal do que criar abstrações que retornam o elemento processado (que só contém as propriedades que a implementação conhece), é criar abstrações que realizem operações em cima da representação original, como acontece com a API do MongoDB, onde você só especifica os atributos que devem ser modificados, mas os atributos não-especificados continuam a existir.

Caso você não seja um guerreiro incorruptível e se desvie do caminho correto, pelo menos tenha a decência de explicitamente proibir, na especificação do padrão, o uso de propriedades adicionais. Tal exigência pode ser especificada como “additionalProperties: false” em JSON Schema.

Final

No final do texto eu fiquei com preguiça e passei a só explicar quais deveriam ser suas preocupações, adicionando referências para você pesquisar mais. Foi diferente do começo do texto, onde eu estava com paciência até para explicar os problemas com detalhes.

Se eu esqueci de alguma coisa ou você tem alguma informação relevante a acrescentar, comente!

BlowThemAll e o GIIEditor

Estou trabalhando no desenvolvimento de um jogo online open source do mesmo gênero que Bomberman chamado BlowThemAll. Recentemente consegui finalizar o editor que será utilizado para a criação de personagens, blocos e bombas (sim, o jogo será completamente customizável).

Antes de falar sobre o editor, acho que devo discutir sobre o problema que ele ajuda a resolver.

O problema

No jogo é possível encontrar vários objetos (personagens, bombas e blocos) que possuem animações. No entanto, existem alguns problemas em criar as animações para o jogo, pois elas possuem características próprias. Nessa seção vou detalhá-las.

O caso de animação mais simples entre os objetos do jogo é a bomba, que segue a mesma lógica de um GIF animado, uma animação estática, em que existe uma sequência de imagens que são processadas sequencialmente, com um intervalo definido entre uma imagem e outra.

Mas não podemos usar imagens GIFs, pois nem todas as animações do jogo seguem a mesma lógica. Como exemplo há os blocos, que, diferente das bombas, se mantêm estáticos, até o momento em que são explodidos e então uma animação ocorre. Diferente das bombas, a animação do bloco é dinâmica, pois pelo menos uma de suas propriedades (nesse caso o tempo em que ele fica estático), só pode ser conhecida depois que o jogo inicia.

Ainda no exemplo do bloco, podemos dizer que sua animação, apesar de dinâmica, é até simples, pois ela é linear, já que sempre segue a mesma sequência. Mas nem todos os objetos do jogo possuem animações que possuem essas características. Há o personagem, por exemplo, cuja animação segue uma sequência não-linear (em um dado momento ele pode estar em um estado de animação que simule o movimento na direção norte, e em outro momento um estado que simule o movimento em outra direção). Normalmente seria possível simular uma animação não-linear com uma animação linear, mas no caso do objeto do personagem, essa parte da animação também é dinâmica, eliminando essa possibilidade.

Princípios que a solução deve seguir

Além de resolver o problema, a solução que devemos adotar deve cumprir algumas promessas, que a adeque as regras de desenvolvimento utilizadas no jogo. Alguns pontos importantes em nosso caso são:

  • Lógica e funcionalidade devem ser separadas de arte e design: Então quando o desenvolvimento iniciar, o designer/artista deve ter liberdade de mudar muitos aspectos da animação sem que seja necessário esforço em conjunto dos desenvolvedores
  • As animações devem utilizar imagens vetoriais e padrões aberto: Queremos que o jogo rode nas principais plataformas disponíveis hoje, então devemos favorecer o uso de padrões abertos. Além disso, o uso de imagens vetoriais é interessante para permitir que o jogo funcione bem em diferentes resoluções de forma eficiente.
  • Deve ser possível armazenar, enviar e trocar o conjunto de animações para cada objeto: Assim cada jogador pode criar um personagem com identidade própria e usar essa identidade visual ao jogar durante as partidas, sejam elas locais ou online.

A solução

A solução que eu criei se apoia em três recursos. O primeiro é um esquema para arquivos INI descreverem o conjunto de animações do objeto. Para descrever essas animações, estados são definidos, e cada estado tem sua própria sequência de imagens, período de transição e ação ao término da animação (que é sempre uma referência a um novo estado ou começo dele próprio). Para permitir o dinamismo necessário no jogo, os estados possuem nomes, permitindo que acontecimentos no jogo mudem o estado do objeto usando esse nome como referência.

O segundo é um conjunto é um conjunto de arquivos de imagens vetoriais armazenadas no formato SVG.

O empacotamento do arquivo INI e dessas imagens compõe o terceiro recurso. O formato de empacotamento que utilizaremos no jogo é o rcc.

A implementação para essa solução foi algo rápido e seu código já encontra-se no repositório do jogo. O vídeo abaixo demonstra o uso dessa tecnologia:

O editor

Para o artista, porém, é algo inconveniente usar técnicas com as quais o programador está acostumado. Eu não conheço muitos artistas que gostariam de trabalhar editando arquivos descritivos e de marcação e usando programas que possuem uma interface de linha de comando para gerar o resultado final.

Para resolver esse outro problema, eu criei um editor com interface gráfica que possui, entre outras, as seguintes características:

  • Gerencia as imagens que serão incluídas no arquivo final
  • Gerencia os estados dos objetos, permitindo que vários estados sejam abertos simultaneamente, agrupados em abas
  • Pré-visualiza a animação do estado particular
  • Suporte a zoom
  • Além de compilar o resultado final, permite criar e salvar projetos
  • Permite criar um projeto vazio ou usando um dos modelos incluídos como base
  • Possui uma interface bastante intuitiva

Um vídeo que eu havia feito durante um dos estágios de desenvolvimento da ferramenta:

Caso queira utilizar essa ferramenta, baixe o código e compile-o usando os comandos:

Em breve devo colocar no repositório AUR para facilitar a vida dos usuários do Archlinux e algum dos outros desenvolvedores deve compilar uma versão para windows e distribuir o executável.

Para quem usa Archlinux: Coloquei o giieditor no AUR: http://aur.archlinux.org/packages.php?ID=51991.

NOTA: Para a funcionalidade “compilar” funcionar, você deve ter o rcc (aplicativo distribuído junto com o Qt) instalado e acessível pelo PATH.

%d blogueiros gostam disto: