Seja bem-vinda ao tutorial do Women Who Go Porto Alegre!
O objetivo deste tutorial é mostrar que programação não é uma coisa de outro mundo e pode ser muito divertido. Nosso objetivo é construir um jogo do zero, utilizando a linguagem de programação Go.
As linguagens de programação são a forma como nós nos comunicamos com os computadores e dizemos para eles o que queremos que eles façam. Além do Go existem inúmeras outras linguagens, cada uma com as suas particularidades.
Assim como uma língua humana, você não precisa saber tudo de uma linguagem de programação para usá-la. Com o tempo você vai aprender recursos mais avançados e formas diferentes de escrever a mesma coisa, algumas melhores, outras piores, ou ainda, apenas diferentes.
Neste tutorial faremos o possível para explicar os principais elementos da linguagem Go, porém não se sinta mal se não entender tudo neste primeiro contato. O mais importante é entender a idéia geral e é claro, se você estiver participando de um evento presencial, não hesite em pedir ajuda para as nossas coaches! :)
No primeiro passo nós vamos preparar o ambiente de desenvolvimento e criar um primeiro programa executável para testar se tudo está funcionando corretamente.
Você vai precisar de:
- Um computador com acesso à internet
Você vai terminar esta etapa com:
- Uma instalação do Go funcionando
- Um editor de texto simples com o qual você se sinta confortável
- Um programa em Go que escreve "Hello Go!" na tela
Nota: para programação nós não utilizamos editores que formatam texto (por exemplo, Microsoft Word), nós usamos os editores de texto simples (em inglês plain-text).
Sugestões de editores de texto:
- MacOS: atom
- Linux: gedit
- Windows: Notepad++
Temos duas opções de como instalar o Go no MacOS: usando Homebrew ou o instalador de pacotes do MacOS.
O jeito mais simples de instalar o Go é usando o Homebrew, que é um gerenciador de pacotes para o MacOS. Para instalar o Homebrew basta rodar o seguinte comando no terminal:
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
O próprio script de intalação do Homebrew explica o que esta fazendo e pausa quando necessário.
Caso você tenha instalado o Homebrew, basta rodar:
brew install go
Caso você tenha optado pelo instalador de pacotes do MacOS, abra a página de downloads do Go procure pelo arquivo do instalador para MacOS e clique no link. Os arquivos serão instalados por padrão em /usr/local/go
.
Em ambos os casos, após a instalação a sua variável de ambiente PATH
já deve conter o binário do go. Para que o comando possa ser utilizado é possível que você tenha que reiniciar as sessões do terminal que estão abertas.
Para garantir que a instalação foi bem sucedida e que o Go foi instalado corretamente, basta rodar o comando go version
e observar uma saída parecida com a seguinte:
❯ go version
go version go1.8.1 darwin/amd64
Se você usa uma distribuição do Linux baseada no Debian, como o Ubuntu, basta rodar o seguinte comando:
sudo apt-get install golang-1.8-go
Para outras distribuições de linux o primeiro passo para instalar o Go é baixar o arquivo .tar.gz, para isso abra a página de downloads do Go procure pelo link correto.
Em seguida será necessário extrair o conteúdo do arquivo baixado em /usr/local
, para criar uma árvore dos arquivos do Go em /usr/local/go
.
sudo tar -C /usr/local -xzf go$VERSION.$OS-$ARCH.tar.gz
Por exemplo, se você baixou o arquivo go1.8.1.linux-amd64.tar.gz
:
sudo tar -C /usr/local -xzf go1.8.1.linux-amd64.tar.gz
Finalmente, tanto para a instalação com apt-get
quanto com o arquivo .tar.gz é necessário adicionar o binário do Go à sua variável de ambiente PATH
. Para fazer isso adicione ao bash profile $HOME/.profile
a linha seguinte:
export PATH=$PATH:/usr/local/go/bin
Para que o comando possa ser utilizado é possível que você tenha que reiniciar as sessões do terminal que estão abertas.
Para garantir que a instalação foi bem sucedida e que o Go foi instalado corretamente, basta rodar o comando go version
e observar uma saída parecida com a seguinte:
ubuntu@svartir-sandar:~$ go version
go version go1.8.1 linux/amd64
O primeiro passo é instalar o Git. O jeito mais fácil é fazer o download do Git For Windows. A vantagem de utilizar o Git For Windows é que também será instalado o Git Bash. No caso desse tutorial, faremos os personagens do jogo usando caracteres Unicode. Infelizmente, o prompt de comando do Windows não consegue exibir os caracteres Unicode, então para esse tutorial usaremos o Git Bash que suporta esses caracteres.
Para instalar o Git For Windows basta clicar no link acima e então em Download. Siga os passos da instalação normalmente até chegar na tela de seleção de componentes. Nessa tela é muito importante selecionar a opção "Use a TrueType font in all console windows". É essa opção que vai fazer com que os caracteres Unicode sejam exibidos corretamente.
Continue seguindo as instruções de instalação até a tela de configuração do emulador de terminal que será utilizado pelo Git Bash. Nessa tela selecione a opção "Use MinTTY (the default terminal of MSYS2)". Juntamente com a opção selecionada anteriormente, essa opção também garantirá a exibição correta dos carcteres Unicode.
Após essa tela continue seguindo as instruções do instalador até que o Git For Windows seja instalado. Ao fim da instalação abra o Git Bash.
O jeito mais simples de instalar o Go no Windows é utilizar o instalador MSI. Na página de downloads do Go procure pelo arquivo do instalador para Windows e clique no link. Os arquivos serão instalados por padrão em C:\Go
.
Após a instalação a sua variável de ambiente PATH
já deve conter o binário do go (C:\Go\bin
). Para que o comando possa ser utilizado é possível que você tenha que reiniciar o Git Bash que está aberto.
Para garantir que a instalação foi bem sucedida e que o Go foi instalado corretamente, basta rodar o comando go version
e observar uma saída parecida com a seguinte:
C:\Users\Camila\Documents\GitHub> go version
go version go1.8.1 windows/amd64
A linguagem Go necessita que todo o código Go esteja localizado em um único workspace. O workspace é uma pasta que contém três sub-pastas: uma pasta src
, que contém todos os arquivos fonte em Go. Uma pasta pkg
, que contém os objetos dos pacotes e uma pasta bin
que contém os comandos executáveis. Para mais detalhes leia a documentação da linguagem.
A localização do workspace é definido por uma varável de ambiente chamada GOPATH
. A localização padrão dessa variável é %USERPROFILE%\go
(geralmente C:\Users\SeuNome\go) no Windows ou $HOME/go
no caso do MacOS e Linux. Nesse tutorial usaremos a localização padrão do workspace. Caso você deseje utilizar outro local será necessário alterar o valor do GOPATH
para que seu programa funcione corretamente.
Para criar o workspace na localização padrão vá até a pasta home e crie a pasta go
. Dentre as três sub-pastas do workspace, apenas a pasta src
precisa ser criada:
cd ~
mkdir go
cd go
mkdir src
cd src
cd %USERPROFILE%
mkdir go
cd go
mkdir src
cd src
Pronto! É dentro desta pasta src
onde você pode começar a construir seu primeiro programa em Go :)
Existe uma tradição na área da computação em que toda vez que você vai começar a aprender uma linguagem nova você comece escrevendo um programa chamado "Hello world".
Nós costumamos fazer isso porque geralmente é bem simples de fazer e nos ajuda a testar se a instalação da linguagem está ok.
Então vamos fazer o mesmo para a linguagem Go.
Coach: explicar resumidamente o que é terminal e pasta, se necessário.
Primeiro, crie uma pasta no seu computador onde você vai guardar os códigos que escrever. Você pode dar qualquer nome para ela, mas de preferência sem acentos ou espaços. Por exemplo: tutorial
.
Digite no terminal:
mkdir tutorial
cd tutorial
Abra um editor de texto simples e copie e cole o trecho a seguir:
package main
import "fmt"
func main() {
fmt.Println("Olá Go!")
}
Salve o arquivo como main.go
na pasta que você criou para os seus códigos.
Vamos compilar o programa. Este termo pode parecer estranho, mas na verdade isso só quer dizer que vamos traduzir o texto que escrevemos na linguagem de programação para uma linguagem que o computador entende (chamada linguagem de máquina).
Para fazer isso, digite no terminal:
go build
Com este comando o Go vai "construir" o programa a partir do texto que você escreveu no arquivo main.go
. Ele vai criar um novo arquivo na mesma pasta que é o chamado "executável".
Vamos rodar este programa. Se você estiver no MacOS ou Linux, o comando para executar é assim:
./tutorial
No Windows, o comando para executar é assim:
tutorial.exe
Você deve ter visto a mensagem "Olá Go!" sendo impressa na tela. Estamos prontas para começar!
Neste tutorial nós vamos construir um jogo chamado PacGo. O nome é uma brincadeira com o clássico PacMan.
Para quem não conhece, o seu objetivo é controlar o PacGo por um labirinto que está repleto de fantasmas. Nos corredores do labirinto existem pastilhas que contam pontos quando o PacGo come elas.
Em alguns pontos estratégicos, existem cogumelos de força que tornam o PacGo temporariamente invencível aos fantasmas (e pode comê-los também, valendo pontos). O objetivo do jogo é comer todas as pastilhas do labirinto antes de perder todas as vidas.
O primeiro passo no desenvolvimento de um jogo é o chamado game design, que é onde estabelecemos as regras e objetivos do jogo.
Como estamos emprestando a idéia do PacGo de um jogo clássico, vamos pular esta etapa e partir direto para a codificação.
Coach: explicar brevemente o jogo Pac Man observando os aspectos de game design
Digite no seu terminal o seguinte comando:
go get github.com/wwg-poa/tutorial
Ele vai baixar automaticamente para você os arquivos iniciais deste projeto.
Vá para a pasta $GOHOME/src/github.com/wwg-poa/tutorial
e abra o arquivo main.go
no seu editor de textos. Você deve ver o código abaixo:
package main
import "fmt"
func main() {
// Inicializar terminal
Inicializa()
defer Finaliza()
// Inicializar labirinto
// Loop principal
for {
// Desenha tela
// Processa entrada do jogador
// Processa movimento dos fantasmas
// Processa colisões
// Dorme
fmt.Println("Olá Go!")
break // Temporário: quebra o loop infinito
}
}
Salve o arquivo. (Lembre-se sempre de salvar o arquivo após cada alteração!)
Coach: explicar o que são comentários, a função main
e o que é um loop.
Note que o nosso programa não faz nada diferente do programa anterior. Porém, nós incluimos alguns comentários com o objetivo de preparar o terreno para as próximas etapas e um loop for
para ser o nosso loop principal do jogo.
Além disso, logo no começo a função main
incluímos as chamadas para as funções Inicializa
e Finaliza
. O objetivo destas funções é preparar o terminal para que ele entenda corretamente as instruções de impressão e os comandos do teclado, e restaurar ele para o modo anterior quando acabarmos (a palavra chave defer
diz para o Go executar a função Finaliza
por último).
O código que faz isto é este aqui:
// Inicializar terminal
Inicializa()
defer Finaliza() // executa no final da função
Note que não definimos estas funções neste arquivo, elas foram definidas para você no arquivo utils.go
. O entendimento destas funções não é necessário para este tutorial, mas caso fique curiosa fique a vontade para explorar este arquivo.
Resumidamente, para fazer um jogo nós precisamos nos preocupar com os seguintes detalhes:
- Preparar os recursos do computador (tela, teclado, etc)
- Carregar os dados do jogo (no caso, o mapa e a posição de cada elemento no mapa)
- Criar um loop principal (pois nós queremos que o jogo continue sempre funcionado até decidirmos que ele deve parar)
- Dentro do loop:
- Desenhar o labirinto na tela
- Processar o movimento do jogador (também chamado de entrada do usuário)
- Processar o movimento dos fantasmas
- Processar colisões, o que quer dizer, verificar se o jogador bateu em algum fantasma
Coach: explicar o papel de cada uma dessas etapas para a construção do jogo.
Todos estes passos estão anotados no código do programa por meio dos comentários.
Digite no terminal o comando go build
para criar o programa pacgo
. Você pode executar o programa que acabou de criar com o comando ./pacgo
(em Linux ou MacOS), ou com o comando pacgo
(em Windows).
Ao executar o pacgo
você vai reparar que o programa parece ter travado o terminal, porém este é o loop infinito do jogo. Para sair do programa, você pode pressionar as teclas control (Ctrl
) e a letra C
simultaneamente (escrevemos Ctrl+C
para abreviar). Lembre-se deste atalho, vamos usá-lo muitas vezes ao longo do tutorial.
A nossa primeira tarefa de codificação vai ser desenhar um labirinto na tela.
Coach: explique em poucas palavras o que é um import e o que são bibliotecas.
Nós vamos criar uma representação do labirinto no programa. Para isso vamos utilizar uma struct
. As structs são a nossa forma de dizer que uma coisa possui várias partes, ou "propriedades". No caso, o nosso labirinto possui uma largura
, uma altura
e um mapa
.
No arquivo main.go
adicione o seguinte código abaixo de import "fmt"
:
type Labirinto struct {
largura int
altura int
mapa []string
}
var labirinto Labirinto
Coach: explicar a diferença entre declaração e definição.
Vamos criar as funções para construir o labirinto e desenhá-lo na tela. Coloque o código abaixo após a linha var labirinto Labirinto
:
func inicializarLabirinto() {
labirinto = Labirinto{
largura: 20,
altura : 10,
mapa : []string{
"####################",
"# F#",
"# #",
"# #",
"# #",
"# #",
"# #",
"# #",
"#G #",
"####################",
},
}
}
func desenhaTela() {
for _, linha := range labirinto.mapa {
fmt.Println(linha)
}
}
Coach: explicar a diferença entre declaração de função e chamada de função.
No mapa, o caractere #
representa as nossas paredes. A letra G
representa a posição inicial do nosso personagem (o PacGo) e o F
representa a posição inicial de um fantasma.
Agora altere a função main
para incluir a chamada para a função inicializarLabirinto
antes do loop principal. Dentro do loop adicione a chamada para desenhaTela
. Finalmente, remova a linha que imprime "Olá Go!".
O código da função main
deve ficar assim:
func main() {
// Inicializar terminal
Inicializa()
defer Finaliza() // executa apenas no fim do programa
// Inicializar labirinto
inicializarLabirinto()
// Loop principal
for {
// Desenha tela
desenhaTela()
// Processa entrada do jogador
// Processa movimento dos fantasmas
// Processa colisões
// Dorme
break
}
}
Vamos executá-lo, mas não esqueça de compilar o programa primeiro. No terminal:
go build
./tutorial
Note que ele imprimiu o labirinto e saiu do programa. Isso é porque colocamos a palavra break
para quebrar o loop infinito.
Por enquanto nosso programa só imprime o labirinto e sai da tela. Nada muito emocionante, certo? Mas antes de começarmos a ver as animações, precisamos preparar um pouco mais o terreno e incluir uma forma do usuário interagir com o programa. Para isso precisamos que o nosso jogo reconheça os comandos do teclado.
Nós estamos particularmente interessadas em 5 teclas: a tecla ESC, que vai ser usada para sair do jogo, e as setas, que vão ser usadas para controlar o PacGo.
Para o computador, cada tecla pressionada no teclado tem um valor númerico especial. Nós lemos qual tecla o usuário pressinou com a função os.Stdin.Read
. O código abaixo faz a leitura apenas das teclas que nos interessam e dá nomes mais amigáveis para elas através do tipo Entrada
.
Copie e cole o código abaixo logo após a linha 3 (import "fmt"
):
import "os"
type Entrada int
// Possiveis entradas do usuário
const (
Nada = iota
ParaCima
ParaBaixo
ParaEsquerda
ParaDireita
SairDoJogo // Tecla ESC
)
func leEntradaDoUsuario() Entrada {
var m Entrada
array := make([]byte, 10)
lido, _ := os.Stdin.Read(array)
if lido == 1 && array[0] == 0x1b {
m = SairDoJogo;
} else if lido == 3 {
if array[0] == 0x1b && array[1] == '[' {
switch array[2] {
case 'A': m = ParaCima
case 'B': m = ParaBaixo
case 'C': m = ParaDireita
case 'D': m = ParaEsquerda
}
}
}
return m
}
Agora que nós sabemos quando o usuário pressionou a tecla ESC
, podemos nos livrar do comando break
no loop principal e deixar o usuário decidir quando quer encerrar o programa.
Altere o loop principal para incluir a chamada para leEntradaDoUsuario
conforme o código abaixo:
// Loop principal
for {
// Desenha tela
desenhaTela()
// Processa entrada do jogador
m := leEntradaDoUsuario()
if m == SairDoJogo { break }
// Processa movimento dos fantasmas
// Processa colisões
// Dorme
}
A linha if m == SairDoJogo { break }
interrompe o jogo toda vez que você pressionar ESC
. Experimente:
go build
./tutorial
Você vai reparar que o jogo fica parado até você pressionar a tecla ESC
sair. Porém, você também deve ter percebido que ao pressionar qualquer outra tecla o jogo imprime novamente o mapa logo abaixo do anterior.
Isto acontece porque a cada passo do loop o computador fica esperando você pressionar uma tecla, parando a execução na chamada da função leEntradaDoUsuario
. Quando a tecla chega, ele "desprende" o programa e executa novamente o loop, passando por desenhaTela
.
Primeiro, vamos fazer a tela ser impressa corretamente.
Coach: explicar como funciona o sistema de coordenadas da tela.
Altere a função desenhaTela()
para incluir uma chamada para LimpaTela()
antes de imprimir o mapa:
func desenhaTela() {
LimpaTela() // adicione esta linha
for _, linha := range labirinto.mapa {
fmt.Println(linha)
}
}
A função limpa tela garante que a tela remove todo o conteúdo do terminal e retorna o cursor para a posição (0, 0) para que o desenho seja feito sempre no mesmo lugar.
Experimente executar o programa novamente. Lembre-se que a tecla para sair é ESC
.
Você deve reparar que agora o programa parece não responder a nenhuma tecla exceto a ESC
... isto acontece na verdade porque nós ainda não programamos as outras teclas para fazer nada.
Agora que nós temos a estrutura de animação pronta, podemos começar a pensar em mover o nosso PacGo (atualmente representado pelo G
no mapa).
Para facilitar o controle do PacGo ao longo de todo o programa, vamos primeiro criar uma estrutura para representá-lo. Cole o código antes da definição da função leEntradaDoUsuario
:
type PacGo struct {
posicao Posicao
figura string
}
var pacgo PacGo
func criarPacGo(posicao Posicao, figura string) {
pacgo = PacGo{
posicao: posicao,
figura: "G",
}
}
Agora vamos alterar a função inicializarLabirinto
para construir o PacGo com a sua posição correta no mapa:
func inicializarLabirinto() {
labirinto = Labirinto{
largura: 20,
altura : 10,
mapa : []string{
"####################",
"# F#",
"# #",
"# #",
"# #",
"# #",
"# #",
"# #",
"#G #",
"####################",
},
}
// O código novo começa aqui
for linha, linhaMapa := range labirinto.mapa {
for coluna, caractere := range linhaMapa {
switch( caractere ) {
case 'G': { criarPacGo( Posicao{linha, coluna}, "G") }
}
}
}
}
Com o PacGo criado podemos movimentá-lo. Copie e cole o código abaixo depois da definição da função desenhaTela
:
func moverPacGo(m Entrada) {
var novaLinha = pacgo.posicao.linha
var novaColuna = pacgo.posicao.coluna
switch m {
case ParaCima:
novaLinha--
if novaLinha < 0 {
novaLinha = labirinto.altura - 1
}
case ParaBaixo:
novaLinha++
if novaLinha >= labirinto.altura {
novaLinha = 0
}
case ParaDireita:
novaColuna++
if novaColuna >= labirinto.largura {
novaColuna = 0
}
case ParaEsquerda:
novaColuna--
if novaColuna < 0 {
novaColuna = labirinto.largura - 1
}
}
conteudoDoMapa := labirinto.mapa[novaLinha][novaColuna]
if conteudoDoMapa != '#' {
pacgo.posicao.linha = novaLinha
pacgo.posicao.coluna = novaColuna
}
}
A função moverPacGo
recebe um sinal de movimento e tenta atualizar a posição atual do PacGo. Porém, se a nova posição cair numa parede (representada pelo caractere #
) a função ignora o movimento.
O próximo passo é alterar o programa principal para chamar esta função toda vez que alguém pressionar uma tecla.
Altere o código abaixo do comentário // Processa entrada do jogador
para o código a seguir:
// Processa entrada do jogador
m := leEntradaDoUsuario()
if m == SairDoJogo {
break
} else {
moverPacGo(m)
}
O último passo vai ser alterar a função desenhaTela
para atualizar a posição do PacGo a cada passada. Modifique o código desta função para que fique igual a função abaixo:
func desenhaTela() {
LimpaTela()
// Imprime mapa
for _, linha := range labirinto.mapa {
for _, char := range linha {
switch char {
case '#': fmt.Print("#")
default: fmt.Print(" ")
}
}
fmt.Println("")
}
// Imprime PacGo
MoveCursor(pacgo.posicao)
fmt.Printf("%s", pacgo.figura)
// Move cursor para fora do labirinto
MoveCursor(Posicao{labirinto.altura + 2, 0})
}
Coach: comentar o impacto das mudanças na função desenhaTela.
Compile o programa e execute-o. Você deve reparar que as setas movem o G
na tela. Estamos fazendo progresso!
Pressione ESC
para sair.
Agora que o nosso PacGo é capaz de se mexer está na hora de animar os fantasmas. Vamos começar definindo uma estrutura para representar os fantasmas no código. Copie e cole a definição da struct
Fantasma após a definição da struct
PacGo:
type Fantasma struct {
posicao Posicao
figura string
}
Assim como para o PacGo a estrutura acima só define a "receita" para construir o fantasma. Precisamos também criar os fantasmas propriamente ditos. Como podem existir mais de um fantasma, ao invés de declarar um único objeto fantasma vamos declarar um array de fantasmas.
Coach: explicar o que é um array.
Copie e cole o código abaixo da definição do PacGo:
var fantasmas []*Fantasma
Também precisamos de uma função para criar fantasmas. Copie e cole o código abaixo da função criarPacGo
:
func criarFantasma(posicao Posicao, figura string) {
fantasma := &Fantasma{posicao: posicao, figura: figura}
fantasmas = append(fantasmas, fantasma)
}
Precisamos alterar a função que inicializa o mapa (inicializarLabirinto
) para chamar a função criarFantasma
toda vez que encontrar um caractere F
. Faça a modificação abaixo:
// Processa caracteres especiais
for linha, linhaMapa := range labirinto.mapa {
for coluna, caractere := range linhaMapa {
switch( caractere ) {
case 'G': { criarPacGo(Posicao{linha, coluna}, "G") }
case 'F': { criarFantasma(Posicao{linha, coluna}, "F") }
}
}
}
A estrutura para criar fantasmas está completa, mas ainda não temos o código que exibe eles. Para isso precisamos alterar a função desenhaTela
:
// Imprime PacGo
MoveCursor(pacgo.posicao)
fmt.Printf("%s", pacgo.figura)
// Imprime fantasmas
for _, fantasma := range fantasmas {
MoveCursor(fantasma.posicao)
fmt.Printf("%s", fantasma.figura)
}
// Move cursor para fora do labirinto
MoveCursor(Posicao{labirinto.altura + 2, 0})
O último passo é o código que move os fantasmas. Crie a função abaixo depois da definição da função moverPacGo
:
func moverFantasmas() {
for _, fantasma := range fantasmas {
// gera um número aleatório entre 0 e 4 (ParaDireita = 3)
var direcao = rand.Intn(ParaDireita+1)
var novaPosicao = fantasma.posicao
// Atualiza posição testando os limites do mapa
switch direcao {
case ParaCima:
novaPosicao.linha--
if novaPosicao.linha < 0 { novaPosicao.linha = labirinto.altura - 1 }
case ParaBaixo:
novaPosicao.linha++
if novaPosicao.linha > labirinto.altura - 1 { novaPosicao.linha = 0 }
case ParaEsquerda:
novaPosicao.coluna--
if novaPosicao.coluna < 0 { novaPosicao.coluna = labirinto.largura - 1 }
case ParaDireita:
novaPosicao.coluna++
if novaPosicao.coluna > labirinto.largura - 1 { novaPosicao.coluna = 0 }
}
// Verifica se a posição nova do mapa é válida
conteudoMapa := labirinto.mapa[novaPosicao.linha][novaPosicao.coluna]
if conteudoMapa != '#' { fantasma.posicao = novaPosicao }
}
}
Como nós estamos utilizando a função rand.Intn
do pacote rand
precisamos adicionar o respectivo import
:
import "math/rand"
E adicione a chamada para esta função abaixo do comentário // Processa movimento dos fantasmas
no loop principal:
// Processa movimento dos fantasmas
moverFantasmas()
Compile o programa e utilize as setas para mover o PacGo. Você vai reparar que o F
que representa o nosso fantasma vai se mover toda vez que você pressionar uma tecla.
Experimente adicionar mais um fantasma no mapa para ver o que acontece.
Coach: Explicar porque o movimento dos fantasmas só ocorre após pressionar uma tecla.
Você deve ter reparado na seção anterior que o nosso jogo "trava" esperando o usuário pressionar uma tecla. Num jogo de verdade é esperado que o movimento dos inimigos seja independente do movimento do jogador. Nós precisamos separar o código que lê as teclas pressionadas pelo usuário do código do loop principal.
Coach: Explicar brevemente os conceitos de goroutine e canais.
Para conseguir este objetivo, vamos utilizar o conceito de goroutines
e canais (channels
). A função de uma goroutine é justamente executar um código separado do código principal.
Porém, como ele vai estar separado, é preciso ter uma maneira de comunicar com o código principal (por exemplo, para informar qual tecla foi pressionada). É aí que entram os canais: são formas de comunicação entre dois códigos que estão executando em paralelo.
Altere a função leEntradaDoUsuario
para ter a seguinte forma:
func leEntradaDoUsuario(canal chan<- Entrada) {
array := make([]byte, 10)
for {
lido, _ := os.Stdin.Read(array)
if lido == 1 && array[0] == 0x1b {
canal <- SairDoJogo
} else if lido == 3 {
if array[0] == 0x1b && array[1] == '[' {
switch array[2] {
case 'A': canal <- ParaCima
case 'B': canal <- ParaBaixo
case 'C': canal <- ParaDireita
case 'D': canal <- ParaEsquerda
}
}
}
}
}
Repare nas seguintes mudanças:
- a função agora recebe um parâmetro do tipo
chan<- Entrada
e não possui retorno. - ao invés de gravar na variável
m
, a função grava na variávelcanal
com o operador<-
ao invés de=
- todo o código está envolto por um loop infinito, o que quer dizer que uma vez chamada esta função vai executar repetidamente até o término do programa
Agora vamos alterar a função main
para chamar a função leEntradaDoUsuario
como uma goroutine:
func main() {
// Inicializar terminal
Inicializa()
defer Finaliza()
// Inicializar labirinto
inicializarLabirinto()
// Cria rotina para ler entradas
canal := make(chan Entrada, 10)
go leEntradaDoUsuario(canal)
// Loop principal
for {
// Desenha tela
desenhaTela()
// Processa entrada do jogador
var tecla Entrada
select {
case tecla = <-canal:
default:
}
if tecla == SairDoJogo {
break
} else {
moverPacGo(tecla)
}
// Processa movimento dos fantasmas
moverFantasmas()
// Processa colisões
// Dorme
}
}
Se você compilar e executar o programa agora, vai reparar que o movimento dos fantasmas não depende mais do movimento do PacGo. Porém, temos um efeito indesejado que é a tela ficar piscando rapidamente.
Isto acontece porque o jogo não está mais "travando" na entrada do usuário antes de atualizar a tela e está atualizando ela o mais rápido possível.
Tipicamente, para dar a ilusão de movimento, os jogos atualizam a tela do jogador várias vezes por segundo, onde cada uma destas telas apresenta uma imagem (também chamada de quadro ou frame) com uma pequena diferença em relação a anterior.
Se atualizarmos a tela poucas vezes por segundo a animação vai parecer travada, mas se atualizarmos rápido demais ocorre o fenômeno de flicker que é o que vocês devem ter percebido agora.
O nosso jogo não possui uma animação muito complexa, então é suficiente atualizar a tela 10 vezes por segundo. O truque para fazer isso é a função dorme
.
Esta função faz com que o programa fique parado pelo número de milisegundos que passarmos como parâmetro. Passando o valor de 100 milisegundos nós conseguimos fazer com que o loop principal seja executado 10 vezes por segundo.
Você vai precisar do import time
:
import "time"
Adicione a declaração de dorme
antes da função main
:
func dorme(milisegundos time.Duration) {
time.Sleep(time.Millisecond * milisegundos)
}
E adicione a sua chamada abaixo do comentário // Dorme
dentro do loop principal:
// Dorme
dorme(100)
Teste novamente o programa. O efeito de flicker deve ter sumido.
Nós já temos toda a funcionalidade básica do jogo pronta, mas ele ainda não se parece com um jogo de verdade. Para deixar o jogo com uma cara mais amigável, vamos utilizar emojis como os nossos gráficos!
Na função inicializarLabirinto
, vamos passar o código dos emojis no lugar das letras G
e F
:
// Processa caracteres especiais
for linha, linhaMapa := range labirinto.mapa {
for coluna, caractere := range linhaMapa {
switch( caractere ) {
case 'G': { criarPacGo(Posicao{linha, coluna}, "\xF0\x9F\x98\x83") }
case 'F': { criarFantasma(Posicao{linha, coluna}, "\xF0\x9F\x91\xBB") }
}
}
}
Além disso, vamos substituir o símbolo #
por paredes de verdade. Na função desenhaTela
, altere o código abaixo para imprimir o muro:
// Imprime mapa
for _, linha := range labirinto.mapa {
for _, char := range linha {
switch char {
case '#': fmt.Print(FundoAzul(" "))
default: fmt.Print(" ")
}
}
fmt.Println("")
}
Compile e execute o programa.
O nosso jogo está ganhando forma, mas para parecer um jogo mesmo, precisamos adicionar algumas funcionalidades. Primeiro, nós precisamos de um placar e uma forma do PacGo ganhar pontos.
Vamos primeiro criar um campo para guardar os pontos na struct PacGo:
type PacGo struct {
posicao Posicao
figura string
pontos int
}
Depois vamos alterar a função desenhaTela
para incluir a impressão do placar e as pastilhas no mapa:
func desenhaTela() {
LimpaTela()
// Imprime placar
MoveCursor(Posicao{0, 0})
placar := fmt.Sprintf("Pontos: %d", pacgo.pontos)
fmt.Println(Vermelho(Intenso(placar)))
// Ajuste para desenhar o mapa embaixo do placar
deslocamento := Posicao{2, 0}
MoveCursor(deslocamento)
// Imprime mapa
for _, linha := range labirinto.mapa {
for _, char := range linha {
switch char {
case '#': fmt.Print(FundoAzul(" "))
case '.': fmt.Print(".")
default: fmt.Print(" ")
}
}
fmt.Println("")
}
// Imprime PacGo
MoveCursor(pacgo.posicao.Soma(deslocamento))
fmt.Printf("%s", pacgo.figura)
// Imprime fantasmas
for _, fantasma := range fantasmas {
MoveCursor(fantasma.posicao.Soma(deslocamento))
fmt.Printf("%s", fantasma.figura)
}
// Move cursor para fora do labirinto
MoveCursor(deslocamento.Soma(Posicao{labirinto.altura + 2, 0}))
}
Finalmente, altere o código da função moverPacGo
onde está a lógica responsável por detectar paredes para incluir a contagem de pontos:
conteudoDoMapa := labirinto.mapa[novaLinha][novaColuna]
if conteudoDoMapa != '#' {
pacgo.posicao.linha = novaLinha
pacgo.posicao.coluna = novaColuna
if conteudoDoMapa == '.' {
pacgo.pontos += 10
// Remove item do mapa
linha := labirinto.mapa[novaLinha]
linha = linha[:novaColuna] + " " + linha[novaColuna+1:]
labirinto.mapa[novaLinha] = linha
}
}
Nós temos um placar funcionando e o nosso PacGo pode ganhar pontos, mas os fantasmas ainda não são uma ameaça. Está na hora de tornar o jogo um pouco mais difícil: vamos incluir a contagem de vidas e a possibilidade dos fantasmas matarem o PacGo.
Toda vez que o PacGo morre ele deve voltar para o seu ponto de origem no mapa. Por isso, precisamos guardar a posição onde ele inicia. Vamos adicionar o número de vidas e a posicição inicial na estrutura PacGo
:
type PacGo struct {
posicao Posicao
posInicial Posicao
figura string
pontos int
vidas int
}
Na criação do PacGo
salvamos a posicição inicial e configuramos o número inicial de vidas:
func criarPacGo(pos Posicao, fig string) {
pacgo = PacGo{
posicao: pos,
posInicial: pos,
figura: fig,
vidas: 2,
}
}
E modificamos o placar para mostrar as vidas:
// Imprime placar
MoveCursor(Posicao{0, 0})
placar := fmt.Sprintf("Pontos: %d Vidas: %d", pacgo.pontos, pacgo.vidas)
fmt.Println(Vermelho(Intenso(placar)))
Agora só falta adicionar a lógica da colisão com os fantasmas. Primeiro vamos criar uma função para buscar um fantasma na posição atual do PacGo:
func detectarColisao() *Fantasma {
for _, fantasma := range fantasmas {
if fantasma.posicao == pacgo.posicao {
return fantasma
}
}
return nil
}
E no loop principal adicione o seguinte código para processar a colisão:
// Processa colisões
if fantasma := detectarColisao(); fantasma != nil {
pacgo.vidas--
if pacgo.vidas < 0 {
MoveCursor(Posicao{labirinto.altura + 3, 0})
fmt.Print("Fim de jogo! Os fantasmas venceram... \xF0\x9F\x98\xAD\n\n")
break
}
// Reseta posição do PacGopher para a posição inicial
pacgo.posicao = pacgo.posInicial
}
Compile o código e execute para ver o resultado!
Na etapa passada nós adicionamos o suporte a vidas, o que permite que o jogador perca o jogo caso acabem todas as suas vidas. Agora precisamos dar condições para que o jogador vença a partida.
O jogo acaba quando o PacGo come todas as pastilhas do labirinto. Para fazer isso, nós precisamos contar quantas pastilhas temos no total e quando este número chegar a zero a partida acabou e o PacGo venceu.
A maneira mais fácil de fazer isso é manter um contador de pastilhas na estrutura labirinto, inicializar com o total de pastilhas no momento da criação do labirinto e decrementar este número toda vez que o PacGo comer uma pastilha.
Na estrutura Labirinto
:
type Labirinto struct {
largura int
altura int
mapa []string
muro string
numPastilhas int
}
Na função inicializarLabirinto
:
// Processa caracteres especiais
for linha, linhaMapa := range labirinto.mapa {
for coluna, caractere := range linhaMapa {
switch caractere {
case 'G':
criarPacGo(Posicao{linha, coluna}, "\xF0\x9F\x98\x83")
case 'F':
criarFantasma(Posicao{linha, coluna}, "\xF0\x9F\x91\xBB")
case '.':
labirinto.numPastilhas++
}
}
}
Na função moverPacGo
:
conteudoDoMapa := labirinto.mapa[novaLinha][novaColuna]
if conteudoDoMapa != '#' {
pacgo.posicao.linha = novaLinha
pacgo.posicao.coluna = novaColuna
if conteudoDoMapa == '.' {
pacgo.pontos += 10
labirinto.numPastilhas--
// Remove item do mapa
linha := labirinto.mapa[novaLinha]
linha = linha[:novaColuna] + " " + linha[novaColuna+1:]
labirinto.mapa[novaLinha] = linha
}
}
No loop principal, adicione o seguinte teste:
// Fim de jogo
if labirinto.numPastilhas == 0 {
MoveCursor(Posicao{labirinto.altura + 3, 0})
fmt.Print("Fim de jogo! Você venceu! \xF0\x9F\x98\x84\n\n")
break
}
Compile e teste! :)
O nosso jogo está quase completo, mas ainda falta um recurso importante! No jogo original, o PacGo pode comer pilulas especiais que dão o poder de comer os fantasmas. O efeito desta pílula é temporárioe dura apenas alguns segundos, mas comer os fantasmas neste estado dá muitos pontos!
Na falta de um emoji pílula nós usamos o cogumelo. (Fique a vontade para escolher qualquer emoji)
type PacGo struct {
posicao Posicao
posInicial Posicao
figura string
pontos int
vidas int
pilula bool
}
A pilula é representada pelo caractere P
no mapa. Altere a função moverPacGo
:
if conteudoDoMapa != '#' {
pacgo.posicao.linha = novaLinha
pacgo.posicao.coluna = novaColuna
if conteudoDoMapa == '.' || conteudoDoMapa == 'P' {
switch conteudoDoMapa {
case '.':
pacgo.pontos += 10
labirinto.numPastilhas--
case 'P':
pacgo.pontos += 100
ativarPilula()
}
// Remove item do mapa
linha := labirinto.mapa[novaLinha]
linha = linha[:novaColuna] + " " + linha[novaColuna+1:]
labirinto.mapa[novaLinha] = linha
}
}
Crie as funções abaixo para ativar e desativar o modo invencível:
func ativarPilula() {
pacgo.pilula = true
go desativarPilula(10000) // 10 segundos
}
func desativarPilula(milisegundos time.Duration) {
dorme(milisegundos)
pacgo.pilula = false
}
// Processa colisões
if fantasma := detectarColisao(); fantasma != nil {
if pacgo.pilula {
go ressucitarFantasma(fantasma, 10000)
matarFantasma(fantasma)
pacgo.pontos += 500
} else {
pacgo.vidas--
// Reseta posição do PacGo para a posição inicial
pacgo.posicao = pacgo.posInicial
}
if pacgo.vidas < 0 {
MoveCursor(Posicao{labirinto.altura + 3, 0})
fmt.Print("Fim de jogo! Os fantasmas venceram... \xF0\x9F\x98\xAD\n\n")
break
}
}
func ressucitarFantasma(fantasma *Fantasma, milisegundos time.Duration) {
dorme(milisegundos)
criarFantasma(fantasma.posicao, fantasma.figura)
}
func matarFantasma(fantasma *Fantasma) {
var indice int
for idx, f := range fantasmas {
if f.posicao == fantasma.posicao {
indice = idx
break
}
}
fantasmas = append(fantasmas[:indice], fantasmas[indice+1:]...)
}
Para que o jogador saiba que ainda está sobre o efeito da pílula, vamos mudar a cor do mapa durante o efeito dela. Altere a função desenhaTela
para testar se a pílula está ativa:
// Imprime mapa
for _, linha := range labirinto.mapa {
for _, char := range linha {
switch char {
case '#':
if pacgo.pilula {
fmt.Print(FundoVermelho(" "))
} else {
fmt.Print(FundoAzul(" "))
}
case '.':
fmt.Print(".")
case 'P':
fmt.Print("\xF0\x9F\x8D\x84")
default:
fmt.Print(" ")
}
}
fmt.Println("")
}
Nosso jogo está quase completo! Vamos agora para a última etapa, que é suportar mapas externos. Para isso vamos precisar ler arquivos do disco e carregá-los em memória.
// Lê parâmetros do sistema operacional
args := os.Args[1:]
var arquivo string
if len(args) >= 1 {
arquivo = args[0]
} else {
arquivo = ""
}
// Inicializar labirinto
inicializarLabirinto(arquivo)
Modifique a função inicializarLabirinto
para carregar o mapa do arquivo ao invés da memória:
func inicializarLabirinto(arquivo string) error {
var ErrMapNotFound = errors.New("Não conseguiu ler o arquivo do mapa")
// aplica o valor default caso não seja passado um arquivo
var tmpArquivo string
tmpArquivo = arquivo
if tmpArquivo == "" {
tmpArquivo = "./mapas/mapa01.txt"
}
// abre arquivo
file, err := os.Open(tmpArquivo)
if err != nil {
log.Fatal(err)
return ErrMapNotFound
}
// fecha depois de ler o arquivo
defer file.Close()
// inicializa o mapa vazio
mapa := []string{}
// cria um leitor para ler linha a linha o arquivo
scanner := bufio.NewScanner(file)
for scanner.Scan() {
linha := scanner.Text()
mapa = append(mapa, linha)
}
// determina o tamanho do mapa baseado no arquivo lido
largura := len(mapa[0])
altura := len(mapa)
labirinto = Labirinto{
largura: largura,
altura: altura,
mapa: mapa,
}
// Processa caracteres especiais
for linha, linhaMapa := range labirinto.mapa {
for coluna, caractere := range linhaMapa {
switch caractere {
case 'G':
criarPacGo(Posicao{linha, coluna}, "\xF0\x9F\x98\x83")
case 'F':
criarFantasma(Posicao{linha, coluna}, "\xF0\x9F\x91\xBB")
case '.':
labirinto.numPastilhas++
}
}
}
return nil
}
Dentro do repositório do projeto existe uma pasta mapas
que contém alguns mapas para testar. Experimente criar o seu próprio mapa!
Parabéns! Ao chegar nesta etapa você já tem um jogo completo funcionando. Mas você não precisa parar por aqui. Pense em que melhorias gostaria de ter no jogo e pesquise como implementá-las. A melhor forma de aprender é na prática, e ter um projeto open source é um ótimo ponto de partida.
Algumas sugestões de melhorias:
- Adicionar um segundo jogador
- Adicionar outros tipos de item no mapa
- Adicionar efeitos especiais (cores)
- Deixar os fantasmas mais inteligentes
As possibilidades são infinitas!
E é claro, divulgue o seu jogo na nossa página no Facebook Women Who Go Brasil e no Twitter mencionando @womenwhogo e @womenwhogo_poa.