Tetris con pantalla OLED SSD1306 y Arduino

El juego Tetris en Arduino Uno con pantalla OLED i2c 128×64 y botones es un proyecto sencillo para principiantes. El esquema de montaje del juego Tetris en Arduino Uno con pantalla es lo más sencillo posible, por lo que cualquier principiante podrá repetirlo. En esta página hemos colocado el esquema de conexión de la pantalla y los botones al Arduino, así como el programa para crear un juego de tetris en display SSD1306 OLED i2c.

Para esta actividad necesitaremos:

  • Arduino Micro / Arduino Mega
  • pantalla oled 128×64 i2c
  • pulsadores y zumbador
  • placa de pruebas
  • cables de conexion
  • librairies Adafruit_SSD1306.h, Adafruit_GFX.h

Tetris es un juego de ordenador desarrollado por un programador ruso en 1984. El objetivo del juego es colocar en el campo de juego bloques de diferentes formas que van cayendo, de manera que después de cada movimiento se obtenga una línea uniforme sin huecos. Una vez que se llena toda la línea, ésta desaparece y el jugador obtiene puntos. El objetivo del juego es conseguir tantos puntos como sea posible antes de que se llene todo el campo.

El juego Tetris con pantalla OLED y Arduino Uno

Juego Tetris en Arduino con pantalla OLED SSD1306 i2c
Como crear juego Tetris en Arduino con pantalla OLED SSD1306 i2c
OLED i2c 0,96 Arduino Uno Arduino Nano Arduino Mega
GND GND GND GND
VDD 5V 5V 5V
SDA A4 A4 20
SCL A5 A5 21

La pantalla OLED está conectada a la placa Arduino a través del protocolo i2c (ver tabla de conexión). Los botones del reloj están conectados a los pines digitales de la placa y los pines se pueden cambiar si es necesario. Los pines para los botones están configurados en modo input_pullup, por lo que no se necesitan resistencias para la conexión. Después de montar el circuito, descarga el código para jugar al Tetris en el Arduino Nano / Uno.

Código Arduino para juego Tetris con pantalla OLED

#include "Wire.h"
#include "Adafruit_GFX.h"
#include "Adafruit_SSD1306.h"

#define WIDTH 64   // ancho de pantalla en píxeles
#define HEIGHT 128 // altura de pantalla en píxeles
#define BUZZER 12  // puerto de conexión del zumbador
#define left 2     // puerto de conexión del pulsador
#define right 5
#define change 3
#define down 4

Adafruit_SSD1306 display(HEIGHT, WIDTH, &Wire, -1);

// matriz de forma "S"
const char pieces_S_l[2][2][4] = {{ {0, 0, 1, 1}, {0, 1, 1, 2} },
                                  { {0, 1, 1, 2}, {1, 1, 0, 0} }};
// matriz de forma "S"
const char pieces_S_r[2][2][4] = {{ {1, 1, 0, 0}, {0, 1, 1, 2} },
                                  { {0, 1, 1, 2}, {0, 0, 1, 1} }};
// matriz de forma "L"
const char pieces_L_l[4][2][4] = {{ {0, 0, 0, 1}, {0, 1, 2, 2} },
                                  { {0, 1, 2, 2}, {1, 1, 1, 0} },
                                  { {0, 1, 1, 1}, {0, 0, 1, 2} },
                                  { {0, 0, 1, 2}, {1, 0, 0, 0} }};
// matriz de forma "cuadrada"
const char pieces_Sq[1][2][4] = {{ {0, 1, 0, 1}, {0, 0, 1, 1} }};
// matriz de forma "T"
const char pieces_T[4][2][4] = {{ {0, 0, 1, 0}, {0, 1, 1, 2} },
                                { {0, 1, 1, 2}, {1, 0, 1, 1} },
                                { {1, 0, 1, 1}, {0, 1, 1, 2} },
                                { {0, 1, 1, 2}, {0, 0, 1, 0} }};
// matriz de forma "I"
const char pieces_l[2][2][4] = {{ {0, 1, 2, 3}, {0, 0, 0, 0} },
                                { {0, 0, 0, 0}, {0, 1, 2, 3}}};
 
const short MARGIN_TOP = 19;
const short MARGIN_LEFT = 3;
const short SIZE = 5;
const short TYPES = 6;
const int MELODY_LENGTH = 10;
const int MELODY_NOTES[MELODY_LENGTH] = {262, 294, 330, 262};
const int MELODY_DURATIONS[MELODY_LENGTH] = {500, 500, 500, 500};

int click[] = { 1047 };
int click_duration[] = { 100 };
int erase[] = { 2093 };
int erase_duration[] = { 100 };
word currentType, nextType, rotation;
short pieceX, pieceY;
short piece[2][4];
int interval = 20, score;
long timer, delayer;
boolean grid[10][18];
boolean b1, b2, b3;

// función de comprobación de filas llenas
void checkLines() {
  boolean full;
  for (short y = 17; y >= 0; y--) {
    full = true;
    for (short x = 0; x < 10; x++) {
      full = full && grid[x][y];
    }
    if (full) { breakLine(y); y++; }
  }
}

// función de borrado de filas rellenas
void breakLine(short line) {
  tone(BUZZER, erase[0], 1000 / erase_duration[0]);
  delay(100);
  noTone(BUZZER);
  for (short y = line; y >= 0; y--) {
    for (short x = 0; x < 10; x++) {
      grid[x][y] = grid[x][y - 1];
    }
  }
  for (short x = 0; x < 10; x++) {
    grid[x][0] = 0;
  }
  display.invertDisplay(true);
  delay(50);
  display.invertDisplay(false);
  score += 10;
}

// función de limpieza de la pantalla
void refresh() {
  display.clearDisplay();
  drawLayout();
  drawGrid();
  drawPiece(currentType, 0, pieceX, pieceY);
  display.display();
}

void drawGrid() {
  for (short x = 0; x < 10; x++)
    for (short y = 0; y < 18; y++)
      if (grid[x][y])
        display.fillRect(MARGIN_LEFT + (SIZE + 1)*x, MARGIN_TOP + (SIZE + 1)*y, SIZE, SIZE, WHITE);
}

// prueba de colisión horizontal
boolean nextHorizontalCollision(short piece[2][4], int amount) {
  for (short i = 0; i < 4; i++) {
    short newX = pieceX + piece[0][i] + amount;
    if (newX > 9 || newX < 0 || grid[newX][pieceY + piece[1][i]])
      return true;
  }
  return false;
}

// prueba de colisión vertical
boolean nextCollision() {
  for (short i = 0; i < 4; i++) {
    short y = pieceY + piece[1][i] + 1;
    short x = pieceX + piece[0][i];
    if (y > 17 || grid[x][y])
      return true;
  }
  return false;
}

// para generar el siguiente bloque
void generate() {
  currentType = nextType;
  nextType = random(TYPES);
  if (currentType != 5)
    pieceX = random(9);
  else
    pieceX = random(7);
  pieceY = 0;
  rotation = 0;
  copyPiece(piece, currentType, rotation);
}
 
void drawPiece(short type, short rotation, short x, short y) {
  for (short i = 0; i < 4; i++)
    display.fillRect(MARGIN_LEFT + (SIZE + 1) * (x + piece[0][i]), MARGIN_TOP + (SIZE + 1) * (y + piece[1][i]), SIZE, SIZE, WHITE);
}
 
void drawNextPiece() {
  short nPiece[2][4];
  copyPiece(nPiece, nextType, 0);
  for (short i = 0; i < 4; i++)
    display.fillRect(50 + 3 * nPiece[0][i], 4 + 3 * nPiece[1][i], 2, 2, WHITE);
}

void copyPiece(short piece[2][4], short type, short rotation) {
  switch (type) {
    case 0: //L_l
      for (short i = 0; i < 4; i++) {
        piece[0][i] = pieces_L_l[rotation][0][i];
        piece[1][i] = pieces_L_l[rotation][1][i];
      }
      break;
    case 1: //S_l
      for (short i = 0; i < 4; i++) {
        piece[0][i] = pieces_S_l[rotation][0][i];
        piece[1][i] = pieces_S_l[rotation][1][i];
      }
      break;
    case 2: //S_r
      for (short i = 0; i < 4; i++) {
        piece[0][i] = pieces_S_r[rotation][0][i];
        piece[1][i] = pieces_S_r[rotation][1][i];
      }
      break;
    case 3: //Sq
      for (short i = 0; i < 4; i++) {
        piece[0][i] = pieces_Sq[0][0][i];
        piece[1][i] = pieces_Sq[0][1][i];
      }
      break;
    case 4: //T
      for (short i = 0; i < 4; i++) {
        piece[0][i] = pieces_T[rotation][0][i];
        piece[1][i] = pieces_T[rotation][1][i];
      }
      break;
    case 5: //l
      for (short i = 0; i < 4; i++) {
        piece[0][i] = pieces_l[rotation][0][i];
        piece[1][i] = pieces_l[rotation][1][i];
      }
      break;
  }
}

short getMaxRotation(short type) {
  if (type == 1 || type == 2 || type == 5)
    return 2;
  else if (type == 0 || type == 4)
    return 4;
  else if (type == 3)
    return 1;
  else
    return 0;
}

boolean canRotate(short rotation) {
  short piece[2][4];
  copyPiece(piece, currentType, rotation);
  return !nextHorizontalCollision(piece, 0);
}

void drawLayout() {
  display.drawLine(0, 15, WIDTH, 15, WHITE);
  display.drawRect(0, 0, WIDTH, HEIGHT, WHITE);
  drawNextPiece();
  char text[6];
  itoa(score, text, 10);
  drawText(text, getNumberLength(score), 7, 4);
}

short getNumberLength(int n) {
  short counter = 1;
  while (n >= 10) {
    n /= 10;
    counter++;
  }
  return counter;
}

void drawText(char text[], short length, int x, int y) {
  display.setTextSize(1);
  display.setTextColor(WHITE);
  display.setCursor(x, y);
  display.cp437(true);
  for (short i = 0; i < length; i++)
    display.write(text[i]);
}

void setup() {
  pinMode(left, INPUT_PULLUP);
  pinMode(right, INPUT_PULLUP);
  pinMode(change, INPUT_PULLUP);
  pinMode(down, INPUT_PULLUP);
  pinMode(BUZZER, OUTPUT);

  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.setRotation(1);

  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(18, 40);
  display.println("TETRIS");
  display.setCursor(15, 60);
  display.println("ARDUINO");
  display.display();
 
  // el juego no empezará hasta que se pulse el botón  "change"
  while (true) {
    if (!digitalRead(change)) {
      if (b3) { break; }
      b3 = false;
    }
    else { b3 = true; }
  }

  display.clearDisplay();

  drawLayout();
  display.display();
  randomSeed(analogRead(0));
  nextType = random(TYPES);
  generate();
  timer = millis();
}

void loop() {
  if (millis() - timer > interval) {
    checkLines();
    refresh();
    if (nextCollision()) {
      for (short i = 0; i < 4; i++)
        grid[pieceX + piece[0][i]][pieceY + piece[1][i]] = 1;
      generate();
    } else
      pieceY++;
    timer = millis();
  }

  // si se pulsa el botón "left"
  if (!digitalRead(left)) {
    tone(BUZZER, click[0], 1000 / click_duration[0]);
    delay(100);
    noTone(BUZZER);
    if (b1) {
      if (!nextHorizontalCollision(piece, -1)) {
        pieceX--;
        refresh();
      }
      b1 = false;
    }
  } else {
    b1 = true;
  }

  // si se pulsa el botón "right"
  if (!digitalRead(right)) {
    tone(BUZZER, click[0], 1000 / click_duration[0]);
    delay(100);
    noTone(BUZZER);
    if (b2) {
      if (!nextHorizontalCollision(piece, 1)) {
        pieceX++;
        refresh();
      }
      b2 = false;
    }
  } else {
    b2 = true;
  }

  // si se pulsa el botón "down"
  if (!digitalRead(down)) {
    interval = 20;
  } else {
    interval = 400;
  }

  // si se pulsa el botón "change"
  if (!digitalRead(change)) {
    tone(BUZZER, click[0], 1000 / click_duration[0]);
    delay(100);
    noTone(BUZZER);
    if (b3) {
      if (rotation == getMaxRotation(currentType) - 1 && canRotate(0)) {
        rotation = 0;
      } else if (canRotate(rotation + 1)) {
        rotation++;
      }
      copyPiece(piece, currentType, rotation);
      refresh();
      b3 = false;
      delayer = millis();
    }
  } else if (millis() - delayer > 50) {
    b3 = true;
  }
}

Explicación del código para juego Tetris con Arduino Nano y OLED:

  1. tras descargar el programa, hay que pulsar el botón superior (change) para iniciar el juego;
  2. el zumbador emitirá sonidos al pulsar los botones; si se desea, puede desconectarse.

Conclusión. Una vez iniciado el juego, aparecerán bloques de diferentes formas en la pantalla OLED SSD1306 y el objetivo del jugador es apilar correctamente los bloques que van cayendo. Con la ayuda de los botones, el jugador puede girar y mover los bloques en la pantalla, de modo que no llenen la pantalla hasta el borde superior. El objetivo del juego de tetris es que las piezas que caen no lleguen al borde superior del campo de juego de la pantalla OLED.


Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *