Bacchin Antoine
Published © GPL3+

Brick Breaker with accelerometer

Classic brick breaker but you control the paddle with an accelerometer, like a Wii's remote

IntermediateFull instructions provided15 hours18
Brick Breaker with accelerometer

Things used in this project

Hardware components

TFT LCD 320x240 3.2" SPI ILI9341
×1
STM32 Nucleo-64 Board
STMicroelectronics STM32 Nucleo-64 Board
×1
Gravity: I2C BMI160 6-Axis Inertial Motion Sensor
DFRobot Gravity: I2C BMI160 6-Axis Inertial Motion Sensor
×1
Breadboard (generic)
Breadboard (generic)
×1
Resistor 4.75k ohm
Resistor 4.75k ohm
×1
Jumper wires (generic)
Jumper wires (generic)
×1

Software apps and online services

VS Code
Microsoft VS Code

Story

Read more

Schematics

capture_projet_PqRLtXcwlE.PNG

I don't have software for that so here's a picture and all pin detail

Code

Brick Breaker V1

C/C++
Comments in french, sorry guys
#include <Arduino.h>
#include <DFRobot_BMI160.h>
#include <Wire.h>
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>

DFRobot_BMI160 bmi160;
const int8_t i2c_addr = 0x68;

// --- Broches écran ---
const uint8_t TFT_DC = 9;
const uint8_t TFT_CS = 10;
const uint8_t TFT_MOSI = 11;
const uint8_t TFT_CLK = 13;
const uint8_t TFT_RST = 2;
const uint8_t TFT_MISO = 12;

Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_MOSI, TFT_CLK, TFT_RST, TFT_MISO); //paramétrage broches écran

// ------------------ Bande score ------------------------
const int SCORE_ZONE_HEIGHT = 25; // hauteur de la bande du score

// ------------------ Lecture accel X --------------------
int16_t readAccelX() {
  uint8_t data[2];
  Wire.beginTransmission(i2c_addr);
  Wire.write(0x12); // registre DATA_12
  if (Wire.endTransmission(false) != 0)
    return 0;

  Wire.requestFrom(i2c_addr, (uint8_t)2);
  if (Wire.available() < 2)
    return 0;

  data[0] = Wire.read();
  data[1] = Wire.read();

  return (int16_t)((data[1] << 8) | data[0]);
}

// ------------------ Variables globales --------------------
float posX = 0, targetX = 0, filtered = 0;
float barX = 0;
int oldBarX = -1;
int barWidth = 50;
int barHeight = 10;
int bottomY;

float ballX, ballY;
float ballVx = 2.5;
float ballVy = -2.0;
int ballR = 6;

int lastBallX = -1;
int lastBallY = -1;

bool gameOver = false;
bool gameWon = false;
int score = 0;

unsigned long lastFrameTime = 0; //variable de "temps"

unsigned long lastSpeedIncrease = 0;  // pour accélération progressive
float speedMultiplier = 1.0;          // facteur d'accélération
const float MAX_SPEED_MULTIPLIER = 2.5; // limite max

// ----------- Définition des briques ---------------------
const int brickHeight = 10;
const int brickRows = 5;
const int brickCols = 8;
bool bricks[brickRows][brickCols];
int brickWidth;

// --------------------------------------------------------
void initBricks() {
  for (int row = 0; row < brickRows; row++) {
    for (int col = 0; col < brickCols; col++) {
      bricks[row][col] = true;
    }
  }
}

// Dessine toutes les briques (appelé au reset)
void drawBricks() {
  for (int row = 0; row < brickRows; row++) {
    for (int col = 0; col < brickCols; col++) {
      if (bricks[row][col]) {
        int brickX = col * brickWidth;
        int brickY = SCORE_ZONE_HEIGHT + row * brickHeight; // <-- décalage pour laisser la bande du score

        uint16_t brickColor;
        switch (row) {
          case 0: brickColor = ILI9341_RED; break;
          case 1: brickColor = ILI9341_GREEN; break;
          case 2: brickColor = ILI9341_BLUE; break;
          case 3: brickColor = ILI9341_YELLOW; break;
          case 4: brickColor = ILI9341_MAGENTA; break;
          default: brickColor = ILI9341_WHITE; break;
        }
        tft.fillRect(brickX, brickY, brickWidth, brickHeight, brickColor);
      } else {
        // s'assure qu'on nettoie la zone si la brique est marquée détruite (utile au reset partiel)
        int brickX = col * brickWidth;
        int brickY = SCORE_ZONE_HEIGHT + row * brickHeight;
        tft.fillRect(brickX, brickY, brickWidth, brickHeight, ILI9341_BLACK);
      }
    }
  }
}

// Détruit (et efface à l'écran) une brique précise
void destroyBrick(int row, int col) {
  if (row < 0 || row >= brickRows || col < 0 || col >= brickCols) return;
  if (!bricks[row][col]) return;

  bricks[row][col] = false;
  int brickX = col * brickWidth;
  int brickY = SCORE_ZONE_HEIGHT + row * brickHeight;
  tft.fillRect(brickX, brickY, brickWidth, brickHeight, ILI9341_BLACK);
  score++;
}

// Vérifie si toutes les briques sont détruites
bool allBricksDestroyed() {
  for (int r = 0; r < brickRows; r++) {
    for (int c = 0; c < brickCols; c++) {
      if (bricks[r][c]) return false;
    }
  }
  return true;
}

// Vérification de collision balle <-> briques (en tenant compte du décalage)
bool checkBrickCollision() {
  for (int row = 0; row < brickRows; row++) {
    for (int col = 0; col < brickCols; col++) {
      if (bricks[row][col]) {
        int brickX = col * brickWidth;
        int brickY = SCORE_ZONE_HEIGHT + row * brickHeight; // <-- décalage
        if (ballX + ballR > brickX && ballX - ballR < brickX + brickWidth &&
            ballY + ballR > brickY && ballY - ballR < brickY + brickHeight) {
          destroyBrick(row, col);
          // On inverse verticalement sans réinitialiser l'amplitude : on garde la vitesse courante
          ballVy = -ballVy;
          return true;
        }
      }
    }
  }
  return false;
}

// Dessine la bande du score (toujours au-dessus)
void drawScore() {
  tft.fillRect(0, 0, tft.width(), SCORE_ZONE_HEIGHT, ILI9341_BLACK);
  tft.setCursor(5, 5);
  tft.setTextSize(2);
  tft.setTextColor(ILI9341_WHITE);
  tft.print("Score: ");
  tft.print(score);
}

// --------------------------------------------------------
void resetGame() {
  tft.fillScreen(ILI9341_BLACK);
  score = 0;
  gameOver = false;
  gameWon = false;
  ballX = tft.width() / 2;
  ballY = tft.height() / 2;
  ballVx = 150.0f;  // 150 pixels/seconde
  ballVy = -130.0f; // 130 pixels/seconde
  barX = tft.width() / 2;
  speedMultiplier = 1.0;
  lastSpeedIncrease = millis();
  initBricks();
  drawBricks();
  drawScore();
  oldBarX = -1; // Pour forcer le dessin de la raquette au premier loop après reset
}

void showVictoryScreen() {
  tft.fillScreen(ILI9341_BLACK);

  tft.setTextColor(ILI9341_GREEN);
  tft.setTextSize(4);
  int16_t x1, y1;
  uint16_t w, h;

  String msg = "Gagne";
  tft.getTextBounds(msg, 0, 0, &x1, &y1, &w, &h);
  tft.setCursor((tft.width() - w) / 2, (tft.height() - h) / 2);
  tft.print(msg);

  delay(3000); // affiche le message pendant 3 secondes

  resetGame();
}


// --------------------------------------------------------
void setup() {
  Serial.begin(115200);
  delay(100);
  Wire.begin();

  lastFrameTime = millis();

  if (bmi160.softReset() != BMI160_OK) {
    Serial.println("reset false");
    while (1);
  }
  if (bmi160.I2cInit(i2c_addr) != BMI160_OK) {
    Serial.println("init false");
    while (1);
  }

  tft.begin();
  tft.setRotation(1);
  tft.fillScreen(ILI9341_BLACK);
  tft.setTextColor(ILI9341_WHITE);
  tft.setTextSize(2);
  tft.setCursor(10, 10);
  tft.println("Casse briques !");

  bottomY = tft.height() - barHeight - 5;
  brickWidth = tft.width() / brickCols;
  delay(1000);
  resetGame();
}

// --------------------------------------------------------
void loop() {

  if (gameOver) {
    tft.setCursor(60, tft.height() / 2);
    tft.setTextColor(ILI9341_RED);
    tft.setTextSize(3);
    tft.println("PERDU !");
    delay(2000);
    resetGame();
    return;
  }

  if (gameWon) {
    showVictoryScreen();
    return;
  }

  // --- Lecture accelX et déplacement de la barre (physique)
  int16_t accelX = readAccelX();
  float ratio = constrain((float)accelX / 16000.0f, -1.0f, 1.0f);
  filtered = 0.8f * filtered + 0.2f * ratio;
  targetX = (tft.width() / 2) + (filtered * (tft.width() / 2 - barWidth / 2));
  barX += (targetX - barX) * 0.35f;
  int newBarX = (int)barX;

  // --- Accélération progressive (physique)
  unsigned long currentTime = millis();
  float dt = (currentTime - lastFrameTime) / 1000.0f; // dt en secondes
  if (dt > 0.05f) dt = 0.05f;  // limite à 50 ms pour éviter les sauts
  lastFrameTime = currentTime;

  if (currentTime - lastSpeedIncrease > 3000) {
    lastSpeedIncrease = currentTime;
    if (speedMultiplier < MAX_SPEED_MULTIPLIER) {
      speedMultiplier += 0.08;
      if (speedMultiplier > MAX_SPEED_MULTIPLIER) speedMultiplier = MAX_SPEED_MULTIPLIER;
    }
  }

  // --- Efface ANCIENNES positions (une seule fois) si nécessaire
  // Efface ancienne balle
  if (lastBallX >= 0 && lastBallY >= 0) {
    tft.fillCircle(lastBallX, lastBallY, ballR, ILI9341_BLACK);
  }

  // Efface ancienne raquette seulement si elle existait et que la nouvelle position est différente
  const int BAR_MOVE_THRESHOLD = 0.5; 
  if (oldBarX >= 0 && abs(newBarX - oldBarX) >= BAR_MOVE_THRESHOLD) {
    tft.fillRect(oldBarX - barWidth / 2, bottomY, barWidth, barHeight, ILI9341_BLACK);
  }

  // --- Mise à jour de la balle (physique) 
  ballX += ballVx * speedMultiplier *dt;
  ballY += ballVy * speedMultiplier *dt;

  // Rebonds latéraux
  if (ballX - ballR <= 0) {
    ballX = ballR;
    ballVx = -ballVx;
  } else if (ballX + ballR >= tft.width()) {
    ballX = tft.width() - ballR;
    ballVx = -ballVx;
  }

  // Rebonds sur la ligne du score
  if (ballY - ballR <= SCORE_ZONE_HEIGHT) {
    ballY = SCORE_ZONE_HEIGHT + ballR;
    ballVy = -ballVy;
  }

  // Collision avec la barre (paddle)
  int barTop = bottomY;
  if (ballY + ballR >= barTop && ballY + ballR <= barTop + barHeight &&
      ballX >= newBarX - barWidth / 2 && ballX <= newBarX + barWidth / 2 &&
      ballVy > 0) {

    ballVy = -abs(ballVy);
    float offset = (ballX - newBarX) / (barWidth / 2.0);
    ballVx = constrain(offset * 150.0f, -150.0f, 150.0f);
  }

  // Si la balle tombe sous la barre -> game over
  if (ballY - ballR > tft.height()) {
    gameOver = true;
    return;
  }

  static int lastScore = -1;
  if (checkBrickCollision()) {
    if (score != lastScore) {
      // Met à jour juste la zone du score sans effacer toute la bande
      tft.fillRect(70, 0, 60, SCORE_ZONE_HEIGHT, ILI9341_BLACK);
      tft.setCursor(70, 5);
      tft.setTextSize(2);
      tft.setTextColor(ILI9341_WHITE);
      tft.print(score);
      lastScore = score;
    }
  }

  // --- Dessine NOUVELLES positions (une seule fois)
  int newBallX = (int)ballX;
  int newBallY = (int)ballY;

  // Dessine la balle si sa position a changé
  if (newBallX != lastBallX || newBallY != lastBallY) {
    tft.fillCircle(newBallX, newBallY, ballR, ILI9341_YELLOW);
    lastBallX = newBallX;
    lastBallY = newBallY;
  }

  // Dessine la raquette seulement si elle a bougé (ou si c'est la première fois)
  if (oldBarX < 0) {
    // première fois : dessine sans effacer
    tft.fillRect(newBarX - barWidth / 2, bottomY, barWidth, barHeight, ILI9341_WHITE);
    oldBarX = newBarX;
  } else if (abs(newBarX - oldBarX) >= BAR_MOVE_THRESHOLD) {
    // effacement déjà fait plus haut : dessine la nouvelle
    tft.fillRect(newBarX - barWidth / 2, bottomY, barWidth, barHeight, ILI9341_WHITE);
    oldBarX = newBarX;
  }
}

Credits

Bacchin Antoine
1 project • 1 follower

Comments