Um jogo para Arduino com display Nokia
Aprendendo a usar displays gráficos Nokia no Arduino, com o código completo de um jogo de paredão.
Minha formação como programador foi nos anos 80, época de computadores com pouca memória, pouca ou nenhuma capacidade gráfica, e de desenvolvedores com muita criatividade.
Em mais de uma ocasião os exercícios em treinamentos envolviam programar jogos como o da cobrinha ou o de paredão, que 2 décadas depois tiveram momentos de popularidade nas telinhas de celulares como os das linhas da Nokia que acabaram recebendo a fama de indestrutíveis.
Esse conhecimento da época é bem útil na hora de programar para os Arduinos e para usar os displays típicos de celulares: ambos estão bem próximos do nível de envolvimento com os recursos que eu tinha, como programador, na época.
Foi assim que eu resolvi desenvolver, tantos anos depois, uma nova versão do jogo de paredão, para conhecer melhor as interfaces de um módulo display Nokia 5110 para Arduino que chegou aqui na bancada.
Esse módulo é basicamente uma interface de acesso para um display LCD que de fato já esteve em um celular da Nokia, ou no estoque (novo ou usado) de alguma empresa de manutenção especializada neles. Existem muitos por aí, o que faz o preço ser baixo, o que por sua vez faz com que existam múltiplas bibliotecas e documentações facilitando seu uso.
Ele tem 84x48 pixels, o que parece minúsculo (e é), mas permite passar muita informação, incluindo um modo texto bem fácil de usar com 6 linhas de 14 caracteres cada.
A configuração física
A pinagem do módulo display LCD Nokia 5110 que eu usei é a que segue:
A imagem acima mostra a parte traseira da placa. Olhando-o de frente, a ordem numérica dos pinos corresponde ao seu posicionamento da direita para a esquerda.
Sua conexão a um Arduino de 3,3V, seguindo as instruções do fabricante, é bem simples:
1 VCC -- 3V3 do Arduino
2 GND -- GND do Arduino
3 SCE -- pino 7 (poderia ser qualquer pino digital)
4 RST -- pino 6 (poderia ser qualquer pino digital)
5 D/C -- pino 5 (poderia ser qualquer pino digital)
6 DN -- pino 11
7 SCLK -- pino 13
8 LED -- pino 9, com resistor de 330Ω entre eles (poderia ser qualquer pino PWM)
Note, entretanto, que a maioria dos Arduinos (incluindo o onipresente Uno, o Nano e vários outros) não são de 3,3V, e sim de 5V.
Para os Arduinos de 5V, se você não quiser recorrer a um conversor de nível, as instruções do fabricante recomendam alternativas, incluindo o uso de resistores para limitar a corrente. Se for a sua opção, mantenha a mesma pinagem que descrevi acima, mas acrescente resistores de 10KΩ aos pinos 4, 5, 6 e 7 do módulo, e um resistor de 1KΩ ao pino 3 do módulo. O resistor do pino 8 também continua lá.
Eu preferi recorrer ao Uno Plus, clone do Arduino Uno mas que inclui um jumper para escolher o nível lógico entre 3,3V e 5V. Ficou fácil ;-)
Há relatos de quem operou esse tipo de módulo ligado a pinos de 5V por meses a fio sem notar problemas, mas não recomendo assumir que a sua experiência será sempre similar a essa. Mas vale a pena observar a documentação específica do seu módulo: alguns tem os pinos acima com outros nomes e em outras posições, e alguns toleram 5V oficialmente, também.
Como programar o display Nokia 5110 no Arduino
Esses displays são bastante populares, e contam com uma série de bibliotecas que os suportam. Eu preferi instalar as 2 bibliotecas complementares oferecidas pela Adafruit, embora a fabricante do meu modelo seja a Sparkfun. São elas: Adafruit_GFX e Adafruit_PCD8544.
Após fazer o download, descompactar, renomear, mover para a pasta de bibliotecas e reiniciar a IDE do Arduino, você já pode usar os exemplos de código que acompanham a biblioteca, ou desenvolver o seu próprio.
O essencial é que a biblioteca SPI seja incluída (além das 2 bibliotecas da Adafruit), que o display seja instanciado informando o número dos pinos do Arduino correspondentes aos pinos SCLK, DIN, D/C, CS e RST (na pinagem que descrevi acima, a instanciação poderia ser assim: Adafruit_PCD8544 display = Adafruit_PCD8544(13, 11, 5, 7, 6);
), e que você inclua o display.begin();
na inicialização do seu programa.
Aí você pode controlar a intensidade do backlight escrevendo (com analogWrite
) valores de 0 a 255 no pino 9 do Arduino. Para escrever e desenhar na tela, use os comandos descritos no Adafruit_GFX.h e bem exemplificados no programa de demonstração que acompanha a biblioteca, que mostra como posicionar texto, desenhar linhas, polígonos, círculos, bitmaps e mais.
Um jogo no Arduino com o display Nokia 5110
Para dominar o uso do display, optei por desenvolver um pequeno jogo para ele. É mais uma variação do popular jogo do paredão, ou bate-rebate, típico dos videogames jurássicos.
A minha versão tem um bom conjunto de requisitos: controla partidas de 3 vidas, gerencia pontuação, fica mais difícil (mais rápido e com raquete menor) a cada 100 pontos e dá vida extra a cada 500 pontos.
E ela serve como exemplo não apenas dos recursos gráficos básicos do display e da biblioteca (as animações da bolinha e da raquete são desenhadas como retângulos), mas também do posicionamento e atualização de texto, tanto na tela de Game Over quanto nos placares.
Além do módulo display conectado na forma como mencionei mais acima, o jogo usa um potenciômetro para servir de controle: girando-o para a esquerda ou para a direita, a raquete vai na direção correspondente.
Eu usei um potenciômetro de 10KΩ com 3 terminais, cujo primeiro terminal está conectado ao pino 2 do Arduino (que eu mantenho em HIGH
), o terceiro terminal está conectado ao GND do Arduino, e o terminal do meio está conectado (por meio de um resistor de 1KΩ) ao pino A2 do Arduino. Você pode ver o potenciômetro em destaque na imagem acima, ele está com uma tampa no estilo botão de volume. Outros potenciômetros podem ser usados, mas consulte suas documentações para saber como usar seus terminais.
A montagem é simples: Arduino, breadboard, jumpers, um potenciômetro, resistores e o módulo display. A inteligência está toda no programa, que veremos a seguir, colorizado para facilitar a explicação, como de hábito:
// nokia-pingpong
// jogo de bate-rebate em um Arduino com display Nokia 5110
// (c) Augusto Campos 2015 - BR-Arduino.org
// licenca ao final deste arquivo.
// exemplo de jogo com controle de partidas, 3 vidas, vida extra a
// cada 500 pontos e dificuldade crescente (velocidade aumenta, largura
// da raquete diminui) a cada 100 pontos.
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_PCD8544.h>
// configuracoes de hardware
const byte pinoBacklight=9;
const byte potDigital=2;
const byte potAnalogico=A2;
Adafruit_PCD8544 display = Adafruit_PCD8544(13, 11, 5, 7, 6);
void setBacklight(byte intensidade) {
analogWrite(pinoBacklight,intensidade);
}
void setup() {
display.begin();
display.setContrast(50);
setBacklight(80);
pinMode(pinoBacklight,OUTPUT);
pinMode(potDigital,OUTPUT);
digitalWrite(potDigital,HIGH);
randomSeed(analogRead(potAnalogico));
gameOver();
}
// descritores do jogo: posicao e direcao da bola, posicao e
// largura da raquete, pontos, vidas
int X=0;
int Y=0;
float aX=1;
float aY=1;
int rY=0;
int rL=15;
long pontos=0;
int vidas=3;
int velocidade=4;
// variaveis auxiliares do jogo
int proxCentena=100;
float Xf=0;
float Yf=0;
int oldX=-1;
int oldY=-1;
int oldrY=-1;
int oldrL=-1;
float oldaX=aX;
float oldaY=aY;
void posicionaBolaXY() {
// apaga a bola da posicao anterior e desenha na atual
if (oldX>=0) display.drawRect(oldX,oldY,4,4,WHITE);
display.drawRect(X,Y,4,4,BLACK);
oldX=X;
oldY=Y;
}
void posicionaRaqueteYL() {
// apaga a raquete da posicao anterior e desenha na atual
if (oldrY>=0) display.drawRect(78,oldrY,2,oldrL,WHITE);
display.drawRect(78,rY,2,rL,BLACK);
oldrY=rY;
oldrL=rL;
}
void atualizaPlacares() {
// informa na tela o numero de vidas e os pontos
display.fillRect(0,40,70,8,WHITE);
display.setCursor(20,40);
display.print(pontos);
display.setCursor(5,40);
display.print(vidas);
}
void gameOver() {
// tela que aguarda o inicio de nova partida, e a inicializa
display.clearDisplay();
display.setTextSize(2);
display.setCursor(10,0);
display.print("GAME");
display.setCursor(30,17);
display.print("OVER");
display.setTextSize(1);
display.setCursor(8,32);
display.print("Mova o botao");
vidas=0;
atualizaPlacares();
display.display();
int a=analogRead(potAnalogico);
while(abs(a-analogRead(potAnalogico))<=100) {
delay(50);
}
pontos=0;
vidas=3;
velocidade=4;
X=0;
Y=0;
aX=1;
aY=1;
rY=0;
rL=15;
display.clearDisplay();
atualizaPlacares();
}
void piscaTela() {
for (int p=0; p<=6; p++) {
setBacklight(250);
delay(140);
setBacklight(25);
delay(60);
}
}
void detectaBatidas() {
oldaX=aX;
oldaY=aY;
if (X==0) aX=1;
if (Y==36) aY=-1;
if (Y==0) aY=1;
if (X==76) {
// verifica se bateu na raquete e prossegue, reduz as
// vidas ou encerra, conforme o caso
if ((Y>=rY-3) & (Y<=rY+rL)) {
aX=-0.65+random(0,90)/100;
pontos+=10;
gerenciaFases();
setBacklight(200);
} else {
vidas--;
Xf=0;
piscaTela();
atualizaPlacares();
if (vidas<0) {
gameOver();
}
}
}
}
void moveRebateBola() {
setBacklight(10+X*0.8);
Xf=Xf+aX;
Yf=Yf+aY;
X=int(Xf);
Y=int(Yf);
if ((oldaY!=aY) | (oldaX!=aX)) {
pontos++;
atualizaPlacares();
gerenciaFases();
}
}
void moveRaquete() {
int potLido=analogRead(potAnalogico);
if (potLido<448) potLido=448;
if (potLido>576) potLido=576;
rY=map(potLido,448,576,40-rL,0);
}
void gerenciaFases() {
if (pontos>proxCentena) {
if ((proxCentena % 500)==0) vidas++;
if (rL>4) rL-=2;
if (velocidade<8) velocidade++;
proxCentena+=100;
}
}
void loop() {
posicionaRaqueteYL();
posicionaBolaXY();
delay(10-velocidade);
detectaBatidas();
moveRebateBola();
moveRaquete();
display.display();
}
/*
© 2015 Augusto Campos http://augustocampos.net/ (13.04.2015)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
Vamos à explicação dos trechos mais interessantes do programa, começando pelo trecho em roxo, que inclui as 3 bibliotecas necessárias, define em que pinos do arduino estão conectados o controle de intensidade do backlight, o primeiro e o terceiro terminais do potenciômetro, e instancia o display com a lista de 5 pinos do módulo (SCLK, DIN, D/C, CS e RST) conforme já vimos acima.
O trecho em verde é a configuração geral, na função obrigatória setup()
. Note que ela inicializa o display, define seu contraste e backlight, configura os pinos conectados ao potenciômetro e inicializa o gerador de números aleatórios (randomSeed), e em seguida simplesmente chama a função gameOver()
, que veremos a seguir, mas que desde já podemos saber que exibe uma mensagem na tela e fica aguardando o jogador girar o potenciômetro para iniciar o jogo – quando ela terminar de esperar, a função obrigatória loop()
iniciará automaticamente, pois o setup()
terá terminado.
O trecho em vermelho tem definições iniciais de variáveis globais que definem os principais elementos do jogo: X e Y são a posição da bolinha, aX e aY são a velocidade dela nos 2 eixos, rY e rL são a posição e largura da raquete, e pontos, vidas e velocidade são auto-explicativas. Logo após vem um bloco em cinza que define variáveis auxiliares que as complementam.
Em seguida vem o trecho marrom com as duas funções que fazem animação da bolinha e da raquete, ambas com a mesma técnica bem simples: primeiro desenham o objeto, usando a mesma cor do fundo da tela, na posição em que ele anteriormente estavam (ou seja, apagam-no). Em seguida os desenham com a cor de frente, na sua posição atual, e armazenam essa posição para poder depois apagar novamente quando voltarem a ser chamadas.
Em azul marinho vêm as duas funções que escrevem texto na tela, e assim exemplificam os importantes métodos que posicionam o cursor, definem o tamanho da fonte (1 é normal, 2 é grande), limpam a tela e escrevem nela. A primeira delas, chamada atualizaPlacares()
, é bem simples: apenas escreve na parte de baixo da tela o número de vidas e de pontos. A outra, gameOver()
, tem bem mais complexidade: escreve texto em tamanho grande e pequeno, aguarda até o potenciômetro ser girado por pelo menos 1/10 de volta, e em seguida inicializa todos os parâmetros do jogo, para que tenha início uma nova partida.
As 4 funções em cor de laranja quase não interferem de forma direta com o display, mas são elementos centrais da mecânica do jogo. A detectaBatidas()
compara a posição da bolinha com os limites da tela, invertendo a velocidade dela (ou seja, rebatendo-a) no eixo correspondente sempre que detecta que ela chegou ao limite. Se o limite for o do lado direito (em que está a raquete), ela verifica se o contato foi na posição em que a raquete está (e rebate/pontua), ou fora da raquete (e perde vida, encerrando o jogo caso as vidas acabem).
Também em laranja, a moveRebateBola()
é a responsável por recalcular a posição da bolinha a partir da sua velocidade nos 2 eixos, eventualmente pontuando quando detecta que houve uma rebatida. A moveRaquete() lê o ângulo do potenciômetro e, por meio da interessante função map()
do Arduino, converte-o em uma nova posição para a raquete. Já a gerenciaFases()
é a mais simples: ela apenas verifica se a pontuação passou de algum múltiplo de 100 ou de 500, aumentando a dificuldade ou dando vida extra, respectivamente.
Finalmente, em salmão, temos o loop()
do Arduino, definido de maneira bem simples porque a complexidade foi terceirizada para as funções que manipulam variáveis globais. A cada rotação do loop, o programa posiciona a animação da raquete e da bolinha, faz uma breve pausa proporcional à velocidade da fase em que o jogo está, e chama as funções necessárias para rebater a bola e mover a raquete.
A última das linhas da função loop()
é especialmente importante: o método display.display()
é responsável pelo único momento em que a tela é efetivamente atualizada. Note que ele é chamado apenas uma vez, ao final de cada loop. Durante uma execução do loop, a raquete e a bolinha podem ter mudado de posição, o número de vidas e os pontos do placar podem ter sido alterados. Nada disso será visível até que o display.display()
seja chamado e atualize o que está visível na tela.
Com isso concluo minha exploração deste versátil e barato display, cujos 84x48 pixels monocromáticos permitem exibir gráficos e também – na configuração default – distribuir 6 linhas de 14 caracteres cada.
Comentar