Week 10 — Output Devices

Group Assignment

Individual Assignment

Learning Outcomes


Overview

For this week, I tested three output devices on my board:

  1. Servo motor
  2. Buzzer
  3. I2C LCD display

Video Links


1. Servo Motor

I installed the ESP32Servo library in Arduino IDE and tested the servo on pin D10. First I verified simple left-right movement, then moved to smoother and more expressive motion.

Servo wiring and test setup on board
Servo setup connected to XIAO ESP32-C3 board.

Step 1 was confirming that the hardware and library were working with a very simple motion test. After that, I moved to a second test to explore better movement quality and behavior design.

AI Request (Prompt)

I connected the servo motor to XIAO ESP32-C3, and it is connected to pin D10.
Give me some code on how to control a servo motor with some interesting motions.

Code 1 - Basic sweep

#include <ESP32Servo.h>

Servo myServo;

void setup() {
  myServo.setPeriodHertz(50);
  myServo.attach(D10, 500, 2400);
}

void loop() {
  // Move 0 -> 180
  for (int pos = 0; pos <= 180; pos += 2) {
    myServo.write(pos);
    delay(15);
  }

  // Move 180 -> 0
  for (int pos = 180; pos >= 0; pos -= 2) {
    myServo.write(pos);
    delay(15);
  }
}

Result 1

Servo motor moved left and right through the full 180 degree range.

Video 1 - Basic servo sweep.

After confirming the basic sweep worked, I wanted to make some more interesting motion for the servo motor. I asked AI to create a different type of movement based on the previous program, with smoother transitions and more expressive behavior.

AI Request (Prompt)

Based on the previous servo program, create a new version with more interesting motion.
Use the same board and pin (XIAO ESP32-C3, D10), and add smooth transitions,
speed variation, and a short wiggle-style movement.

Code 2 - Smooth and varied motion

#include <ESP32Servo.h>

Servo myServo;

void smoothMove(int startPos, int endPos, int stepDelay) {
  if (startPos < endPos) {
    for (int pos = startPos; pos <= endPos; pos++) {
      myServo.write(pos);
      delay(stepDelay);
    }
  } else {
    for (int pos = startPos; pos >= endPos; pos--) {
      myServo.write(pos);
      delay(stepDelay);
    }
  }
}

void setup() {
  myServo.setPeriodHertz(50);
  myServo.attach(D10, 500, 2400);
  myServo.write(90);
  delay(1000);
}

void loop() {
  smoothMove(90, 20, 12);
  delay(300);

  smoothMove(20, 160, 6);
  delay(300);

  smoothMove(160, 90, 10);
  delay(500);

  myServo.write(70); delay(120);
  myServo.write(110); delay(120);
  myServo.write(80); delay(120);
  myServo.write(100); delay(120);

  myServo.write(90);
  delay(1000);
}

Result 2

Servo movement became smoother, faster in some segments, and included short "wiggle" actions.

Video 2 - Advanced servo motion sequence.

2. Buzzer

I connected a buzzer to D10 and tested two interactions using a button: single-click for a short blaster sound and double-click to start a melody.

In this step, I wanted to practice output behavior based on user input patterns. The goal was to build two clear interactions: short press for sound effect and multi-click behavior for melody.

ChatGPT Request

Write Arduino code for a buzzer connected to pin D10 and a button connected to pin D7 on a XIAO ESP32-C3.
When I press the button once, it should play a short sci-fi blaster shot sound.
When I double-click the button, it should start playing a Star Wars-style melody on the buzzer.
By default, there should be no sound.

Code - Button + buzzer interaction

const int buzzerPin = D10;
const int buttonPin = D7;

// ---------- Notes ----------
#define NOTE_C4 262
#define NOTE_D4 294
#define NOTE_E4 330
#define NOTE_F4 349
#define NOTE_FS4 370
#define NOTE_G4 392
#define NOTE_GS4 415
#define NOTE_A4 440
#define NOTE_B4 494
#define NOTE_C5 523
#define NOTE_D5 587
#define NOTE_E5 659
#define NOTE_F5 698
#define NOTE_FS5 740
#define NOTE_G5 784
#define NOTE_GS5 831
#define NOTE_A5 880

// ---------- Melody ----------
int melody[] = {
  NOTE_A4, NOTE_A4, NOTE_A4,
  NOTE_F4, NOTE_C5,
  NOTE_A4, NOTE_F4, NOTE_C5, NOTE_A4,

  NOTE_E5, NOTE_E5, NOTE_E5,
  NOTE_F5, NOTE_C5,
  NOTE_GS4, NOTE_F4, NOTE_C5, NOTE_A4,

  NOTE_A5, NOTE_A4, NOTE_A4,
  NOTE_A5, NOTE_GS5, NOTE_G5, NOTE_FS5, NOTE_F5,
  NOTE_FS5
};

int noteDurations[] = {
  500, 500, 500,
  350, 150,
  500, 350, 150, 650,

  500, 500, 500,
  350, 150,
  500, 350, 150, 650,

  500, 350, 150,
  500, 250, 250, 125, 125, 250,
  250
};

// ---------- Button handling ----------
bool lastButtonReading = HIGH;
unsigned long lastDebounceTime = 0;
const unsigned long debounceDelay = 30;

int clickCount = 0;
unsigned long firstClickTime = 0;
const unsigned long doubleClickGap = 350;

// ---------- Music state ----------
bool musicPlaying = false;
int currentNote = 0;
unsigned long noteStartTime = 0;
unsigned long noteTotalTime = 0;
bool noteActive = false;

void setup() {
  pinMode(buzzerPin, OUTPUT);
  pinMode(buttonPin, INPUT_PULLUP);
}

// Aggressive sci-fi shoot sound
void playShootSound() {
  noTone(buzzerPin);

  for (int burst = 0; burst < 3; burst++) {
    for (int freq = 2200; freq >= 700; freq -= 120) {
      tone(buzzerPin, freq);
      delay(10);
    }
    noTone(buzzerPin);
    delay(15);
  }

  noTone(buzzerPin);
}

// Start melody from beginning
void startMusic() {
  musicPlaying = true;
  currentNote = 0;
  noteActive = false;
}

// Stop melody
void stopMusic() {
  musicPlaying = false;
  currentNote = 0;
  noteActive = false;
  noTone(buzzerPin);
}

// Non-blocking melody player
void updateMusic() {
  if (!musicPlaying) return;

  int totalNotes = sizeof(melody) / sizeof(melody[0]);

  if (currentNote >= totalNotes) {
    stopMusic();
    return;
  }

  if (!noteActive) {
    int duration = noteDurations[currentNote];
    tone(buzzerPin, melody[currentNote]);
    noteStartTime = millis();
    noteTotalTime = duration;
    noteActive = true;
    return;
  }

  unsigned long elapsed = millis() - noteStartTime;

  if (elapsed >= noteTotalTime) {
    noTone(buzzerPin);
  }

  if (elapsed >= (unsigned long)(noteTotalTime * 1.3)) {
    currentNote++;
    noteActive = false;
  }
}

// Detect click and single/double click logic
void updateButton() {
  bool reading = digitalRead(buttonPin);

  if (reading != lastButtonReading) {
    lastDebounceTime = millis();
    lastButtonReading = reading;
  }

  if ((millis() - lastDebounceTime) > debounceDelay) {
    static bool lastStableState = HIGH;

    if (reading != lastStableState) {
      lastStableState = reading;

      if (lastStableState == LOW) {  // button pressed
        clickCount++;

        if (clickCount == 1) {
          firstClickTime = millis();
        } else if (clickCount == 2) {
          // Double click detected
          clickCount = 0;
          noTone(buzzerPin);
          startMusic();
        }
      }
    }
  }

  // If only one click happened and time expired -> single click action
  if (clickCount == 1 && (millis() - firstClickTime > doubleClickGap)) {
    clickCount = 0;
    if (!musicPlaying) {
      playShootSound();
    }
  }
}

void loop() {
  updateButton();
  updateMusic();
}

Result 3

The buzzer successfully generated effects on button interaction, and this test helped me practice timing, debounce logic, and event-based behavior.

Video 3 - Buzzer output test.

3. I2C LCD Display

For the I2C screen, I used four connections: VCC, GND, SDA, and SCL. On my board, SDA = D4 and SCL = D5.

I split this into two parts: first, verify basic I2C communication and LCD output; second, reuse the working setup to test a simple interactive mini-game with a button.

XIAO ESP32 pin map reference for I2C pins
Pin map reference used to identify I2C pins.
I2C display wiring photo 1
Wiring photo of LCD and board.
I2C display wiring photo 2
Close-up of LCD wiring.

I2C (Inter-Integrated Circuit) is a communication protocol that allows a microcontroller to exchange data with other devices using only two signal lines: SDA and SCL. This is why an I2C display usually has four pins in total: VCC, GND, SDA, and SCL. VCC provides power to the display, and GND provides the ground connection. The two other pins are used for communication. SDA stands for Serial Data, and it carries the actual information being sent between the microcontroller and the display. SCL stands for Serial Clock, and it provides the timing signal that tells the devices when to read or send each bit of data.

The communication works in a synchronized way. The SDA line carries the bits of information, while the SCL line provides the clock pulses. Each time the clock changes, the receiving device knows when to read the next bit from the data line. In this way, the display receives the information one bit at a time, reconstructs the bytes, and then processes them according to whether they are commands or display data.

I2C connection diagram snippet
Quick wiring diagram for SDA/SCL mapping.

AI Request (Prompt)

My I2C display is connected with VCC and GND.
SCL is connected to pin D5 and SDA is connected to pin D4.
Make a simple program for the screen just to check that it works.

Code - I2C LCD check program

#include <Wire.h>
#include <LiquidCrystal_I2C.h>

LiquidCrystal_I2C lcd(0x20, 16, 2);

void setup() {
  Wire.begin(D4, D5);
  lcd.init();
  lcd.backlight();

  lcd.setCursor(0, 0);
  lcd.print("Hello Yaro!");

  lcd.setCursor(0, 1);
  lcd.print("LCD works");
}


void loop() {
}
LCD game development photo
LCD game development and test setup.

LCD screen is working, and now I can write and experiment with different code.

LCD Mini-Game (Button on D7)

I also tested a simple obstacle-jump style game idea on the LCD using button input from D7.

AI Request (Prompt)

Make a simple interaction game for a 16x2 I2C LCD and a button on D7.
Maybe something like a simple jump-over-obstacle game.
Use XIAO ESP32-C3 with SDA on D4 and SCL on D5.

Code - I2C LCD Button jump game

#include <Wire.h>
#include <LiquidCrystal_I2C.h>

LiquidCrystal_I2C lcd(0x20, 16, 2);   // change to 0x7C if needed

const int buttonPin = D7;

bool lastButtonState = HIGH;
unsigned long lastDebounceTime = 0;
const unsigned long debounceDelay = 30;

int playerRow = 1;          // 1 = ground, 0 = jump
bool isJumping = false;
unsigned long jumpStartTime = 0;
const unsigned long jumpDuration = 500;

int obstacleCol = 15;
int score = 0;
bool gameOver = false;
unsigned long lastStepTime = 0;
int gameSpeed = 250;        // smaller = faster

void setup() {
  pinMode(buttonPin, INPUT_PULLUP);

  Wire.begin(D4, D5);
  lcd.init();
  lcd.backlight();

  showStartScreen();
}

void loop() {
  if (gameOver) {
    if (buttonPressed()) {
      resetGame();
    }
    return;
  }

  if (buttonPressed() && !isJumping) {
    isJumping = true;
    jumpStartTime = millis();
    playerRow = 0;
  }

  if (isJumping && millis() - jumpStartTime > jumpDuration) {
    isJumping = false;
    playerRow = 1;
  }

  if (millis() - lastStepTime > gameSpeed) {
    lastStepTime = millis();
    updateGame();
    drawGame();
  }
}

bool buttonPressed() {
  bool reading = digitalRead(buttonPin);

  if (reading != lastButtonState) {
    lastDebounceTime = millis();
    lastButtonState = reading;
  }

  if ((millis() - lastDebounceTime) > debounceDelay) {
    static bool stableState = HIGH;

    if (reading != stableState) {
      stableState = reading;
      if (stableState == LOW) {
        return true;
      }
    }
  }

  return false;
}

void showStartScreen() {
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Mini Jump Game");
  lcd.setCursor(0, 1);
  lcd.print("Press btn start");

  while (!buttonPressed()) {
    delay(10);
  }

  resetGame();
}

void resetGame() {
  playerRow = 1;
  isJumping = false;
  obstacleCol = 15;
  score = 0;
  gameOver = false;
  gameSpeed = 250;
  lcd.clear();
  drawGame();
}

void updateGame() {
  obstacleCol--;

  if (obstacleCol < 0) {
    obstacleCol = 15;
    score++;

    if (gameSpeed > 120) {
      gameSpeed -= 10;   // gradually increase difficulty
    }
  }

  if (obstacleCol == 1 && playerRow == 1) {
    gameOver = true;
    showGameOver();
  }
}

void drawGame() {
  lcd.clear();

  // player
  lcd.setCursor(1, playerRow);
  lcd.print("P");

  // obstacle
  lcd.setCursor(obstacleCol, 1);
  lcd.print("#");

  // score
  lcd.setCursor(10, 0);
  lcd.print("S:");
  lcd.print(score);
}

void showGameOver() {
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Game Over!");
  lcd.setCursor(0, 1);
  lcd.print("Score:");
  lcd.print(score);
  delay(1500);

  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Press button");
  lcd.setCursor(0, 1);
  lcd.print("to restart");
}
LCD game running on screen
LCD game behavior after deployment.
Video 4 - I2C LCD game demo.

4. Space Invaders on QPad (RP2040 + OLED)

Next, I decided to build a QPad board based on Quentin Bolsee's QPad project. I used QPad Xiao because I have an RP2040 microcontroller.

QPad source: https://gitlab.cba.mit.edu/quentinbolsee/qpad-xiao

QPad milling and board preparation photo 1
Milled board .
QPad milling and board preparation photo 2
Drilled holes by drill press

On Mods project (https://modsproject.org/) I found a mill-2D PCB with G-code output that fits my Lunyee 3018 CNC machine. It was also an experiment to use the new CNC milling machine and different end mills.

With my CNC machine, I do not have tiny bits (or enough experience yet) for drilling all holes with the same setup, so I drilled the holes using a drill press. For components layout, I soldered pin headers so the XIAO RP2040 and display can be connected on top without permanent soldering. I also soldered two resistors. After testing with hello world, the display worked correctly.

QPad board components and pin setup
Header/pin setup for removable modules.
Tetris demo on QPad display
Tetris test from Quentin's repository.

Tetris reference used for initial testing: qpad_tetris (Quentin Bolsee)

Based on the existing code, I created several requests to ChatGPT to build a Space Invaders game. After the first version, I used multiple additional prompts to improve behavior and visuals.

AI Request (Prompt) 1

Make a space invaders game based on the tetris code and device configuration.
Also make an animation preview screen with cool graphics for my small screem.

AI Request (Prompt) 2

The code is working, the platform is moving, but it's not enemies, no space invaders coming,
which I can shoot. Also, you can make platform looks more beautiful,
and space invaders also can have different shapes.

AI Request (Prompt) 3

Okay, good, it's working, but when I finish, it's no next level.
Make at least three levels with different types of invaders.
Also, the welcome screen can be different graphics, maybe some simple animation,
several frames of animation like Space Invaders, and put the text a bit higher
and maybe some characters and platform fighting.

AI Request (Prompt) 4

Okay good, it's a few adjustments:
- I want you to add some space and whether the invaders' text can be in the center
  because the last word is out of the screen.
- You can add text by Yaro underneath. Maybe this text by Yaro can be on the left side;
  then on the right side it can be a 3D animation of the space invader monster face.
- Also don't forget to make the space invaders monster and the platform interesting shapes.
- When the game is over, when next level, it can be the message "Level complete. Press B for the next level"
  and put the invaders a little bit higher because they are too low and a little bit more dense
  with each other in the vertical direction.
- Also when the invaders reach the bottom or hit the platform, it's game over.
  In place press B to restart.

AI Request (Prompt) 5

It's one strange thing when I kill invaders from the bottom line,
but it's still some invaders on the top line. I still have game over
when probably bottom line is without any invaders, which is a bottom check.
Please to be sure that only invaders which are not eliminated can reach the bottom
and cause game over.

Code - Space Invaders V4


// SPACE INVADERS V4
// XIAO RP2040 + SSD1306 OLED + touch pads
// Updated with centered title, intro art, denser/higher invaders,
// level complete screen, game over rules, and improved sprite shapes.
#include <Arduino.h>
#include <U8g2lib.h>
#include <Wire.h>
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
static const uint8_t PIN_LEFT = D1;
static const uint8_t PIN_RIGHT = D7;
static const uint8_t PIN_A = D9;
static const uint8_t PIN_B = D10;
#define N_TOUCH 4
#define THRESHOLD 6
int touch_pins[N_TOUCH] = { PIN_LEFT, PIN_RIGHT, PIN_A, PIN_B };
bool nowT[N_TOUCH] = {false, false, false, false};
bool pastT[N_TOUCH] = {false, false, false, false};
void update_touch() {
  for (int i = 0; i < N_TOUCH; i++) {
    int p = touch_pins[i];
    pinMode(p, OUTPUT);
    digitalWrite(p, LOW);
    delayMicroseconds(20);
    pinMode(p, INPUT_PULLUP);
    int t = 0;
    while (!digitalRead(p) && t < 200) t++;
    pastT[i] = nowT[i];
    nowT[i] = (t > THRESHOLD);
  }
}
bool pressed(int i) { return nowT[i] && !pastT[i]; }
bool held(int i) { return nowT[i]; }
enum { T_LEFT = 0, T_RIGHT = 1, T_A = 2, T_B = 3 };
// ================= GAME CONSTANTS =================
#define EC 5
#define ER 3
#define MAX_BULLETS 4
const int PLAYER_Y = 58;
const int PLAYER_W = 12;
const int ENEMY_W = 10;
const int ENEMY_H = 8;
const int ENEMY_GAP_X = 5;
const int ENEMY_GAP_Y = 4;   // denser vertically
const int ENEMY_START_X = 18;
const int ENEMY_START_Y = 10; // higher
// ================= GAME STATE =================
int px = 58;
int level = 1;
int ex = ENEMY_START_X;
int ey = ENEMY_START_Y;
int dir = 1;
int enemyStepDelay = 22;
int enemyFrame = 0;
unsigned long lastEnemyMove = 0;
bool enemy[ER][EC];
bool intro = true;
bool gameOver = false;
bool levelComplete = false;
int completedLevel = 1;
unsigned long introTick = 0;
struct Bullet {
  int x;
  int y;
  bool a;
};
Bullet bullets[MAX_BULLETS];
// ================= HELPERS =================
void clearBullets() {
  for (int i = 0; i < MAX_BULLETS; i++) bullets[i].a = false;
}
void initEnemies() {
  for (int r = 0; r < ER; r++) {
    for (int c = 0; c < EC; c++) {
      enemy[r][c] = true;
    }
  }
  ex = ENEMY_START_X;
  ey = ENEMY_START_Y;
  dir = 1;
  clearBullets();
}
bool enemiesAlive() {
  for (int r = 0; r < ER; r++) {
    for (int c = 0; c < EC; c++) {
      if (enemy[r][c]) return true;
    }
  }
  return false;
}
void startGame() {
  level = 1;
  px = 58;
  gameOver = false;
  levelComplete = false;
  initEnemies();
}
void startNextLevel() {
  level++;
  if (level > 3) level = 1;
  levelComplete = false;
  px = 58;
  initEnemies();
}
// ================= PLAYER =================
void drawPlayer() {
  // more interesting ship shape
  u8g2.drawPixel(px + 5, PLAYER_Y - 3);
  u8g2.drawBox(px + 4, PLAYER_Y - 2, 3, 1);
  u8g2.drawBox(px + 3, PLAYER_Y - 1, 5, 1);
  u8g2.drawBox(px + 2, PLAYER_Y, 7, 1);
  u8g2.drawBox(px + 1, PLAYER_Y + 1, 9, 1);
  u8g2.drawBox(px, PLAYER_Y + 2, 12, 1);
  u8g2.drawPixel(px + 1, PLAYER_Y + 3);
  u8g2.drawPixel(px + 10, PLAYER_Y + 3);
}
// ================= BULLETS =================
void fire() {
  for (int i = 0; i < MAX_BULLETS; i++) {
    if (!bullets[i].a) {
      bullets[i].x = px + PLAYER_W / 2;
      bullets[i].y = PLAYER_Y - 5;
      bullets[i].a = true;
      break;
    }
  }
}
void updateBullets() {
  for (int i = 0; i < MAX_BULLETS; i++) {
    if (bullets[i].a) {
      bullets[i].y -= 3;
      if (bullets[i].y < 0) bullets[i].a = false;
    }
  }
}
void drawBullets() {
  for (int i = 0; i < MAX_BULLETS; i++) {
    if (bullets[i].a) u8g2.drawVLine(bullets[i].x, bullets[i].y, 3);
  }
}
// ================= ENEMIES =================
void drawEnemyType1(int x, int y, bool phase) {
  u8g2.drawBox(x + 2, y, 6, 1);
  u8g2.drawBox(x + 1, y + 1, 8, 1);
  u8g2.drawBox(x, y + 2, 10, 1);
  u8g2.drawPixel(x + 2, y + 3);
  u8g2.drawPixel(x + 7, y + 3);
  if (phase) {
    u8g2.drawLine(x + 1, y + 4, x + 3, y + 6);
    u8g2.drawLine(x + 8, y + 4, x + 6, y + 6);
  } else {
    u8g2.drawLine(x + 2, y + 4, x + 1, y + 6);
    u8g2.drawLine(x + 7, y + 4, x + 8, y + 6);
  }
}
void drawEnemyType2(int x, int y, bool phase) {
  u8g2.drawFrame(x + 1, y + 1, 8, 4);
  u8g2.drawPixel(x, y + 2);
  u8g2.drawPixel(x + 9, y + 2);
  u8g2.drawPixel(x + 3, y + 2);
  u8g2.drawPixel(x + 6, y + 2);
  if (phase) {
    u8g2.drawLine(x + 2, y + 5, x + 1, y + 6);
    u8g2.drawLine(x + 7, y + 5, x + 8, y + 6);
  } else {
    u8g2.drawLine(x + 3, y + 5, x + 3, y + 6);
    u8g2.drawLine(x + 6, y + 5, x + 6, y + 6);
  }
}
void drawEnemyType3(int x, int y, bool phase) {
  u8g2.drawPixel(x + 4, y);
  u8g2.drawBox(x + 2, y + 1, 5, 1);
  u8g2.drawBox(x + 1, y + 2, 7, 1);
  u8g2.drawBox(x, y + 3, 9, 1);
  u8g2.drawPixel(x + 2, y + 4);
  u8g2.drawPixel(x + 6, y + 4);
  if (phase) {
    u8g2.drawLine(x + 2, y + 5, x + 1, y + 6);
    u8g2.drawLine(x + 6, y + 5, x + 7, y + 6);
  } else {
    u8g2.drawLine(x + 3, y + 5, x + 3, y + 6);
    u8g2.drawLine(x + 5, y + 5, x + 5, y + 6);
  }
}
void drawEnemy(int x, int y, int rowType) {
  bool phase = ((millis() / 220) % 2) == 0;
  if (level == 1) {
    drawEnemyType1(x, y, phase);
  } else if (level == 2) {
    drawEnemyType2(x, y, phase);
  } else {
    // mix on level 3
    if (rowType == 0) drawEnemyType1(x, y, phase);
    else if (rowType == 1) drawEnemyType2(x, y, phase);
    else drawEnemyType3(x, y, phase);
  }
}
void drawEnemies() {
  for (int r = 0; r < ER; r++) {
    for (int c = 0; c < EC; c++) {
      if (enemy[r][c]) {
        int x = ex + c * (ENEMY_W + ENEMY_GAP_X);
        int y = ey + r * (ENEMY_H + ENEMY_GAP_Y);
        drawEnemy(x, y, r);
      }
    }
  }
}
void updateEnemies() {
  if (millis() - lastEnemyMove < (unsigned long)enemyStepDelay) return;
  lastEnemyMove = millis();
  enemyFrame++;
  int speed = level;
  ex += dir * speed;
  // Find bounds using only alive invaders
  int leftMost = 999;
  int rightMost = -999;
  int bottomMost = -999;
  for (int r = 0; r < ER; r++) {
    for (int c = 0; c < EC; c++) {
      if (!enemy[r][c]) continue;
      int x = ex + c * (ENEMY_W + ENEMY_GAP_X);
      int y = ey + r * (ENEMY_H + ENEMY_GAP_Y);
      if (x < leftMost) leftMost = x;
      if (x + ENEMY_W > rightMost) rightMost = x + ENEMY_W;
      if (y + ENEMY_H > bottomMost) bottomMost = y + ENEMY_H;
    }
  }
  if (leftMost == 999) return; // no alive invaders
  if (leftMost < 2 || rightMost > 126) {
    dir = -dir;
    ex += dir * speed;
    ey += 3;
    // recompute bottom after stepping down
    bottomMost = -999;
    for (int r = 0; r < ER; r++) {
      for (int c = 0; c < EC; c++) {
        if (!enemy[r][c]) continue;
        int y = ey + r * (ENEMY_H + ENEMY_GAP_Y);
        if (y + ENEMY_H > bottomMost) bottomMost = y + ENEMY_H;
      }
    }
  }
  // Game over only if a STILL-ALIVE invader reaches the base height
  if (bottomMost >= PLAYER_Y - 2) {
    gameOver = true;
  }
  // Game over only if a STILL-ALIVE invader touches the player area
  for (int r = 0; r < ER; r++) {
    for (int c = 0; c < EC; c++) {
      if (!enemy[r][c]) continue;
      int x = ex + c * (ENEMY_W + ENEMY_GAP_X);
      int y = ey + r * (ENEMY_H + ENEMY_GAP_Y);
      bool hitPlayerZone = !(x + ENEMY_W < px || x > px + PLAYER_W ||
y + ENEMY_H < PLAYER_Y - 4 || y > PLAYER_Y + 3);
      if (hitPlayerZone) {
        gameOver = true;
      }
    }
  }
}
// ================= COLLISION =================
void checkHit() {
  for (int i = 0; i < MAX_BULLETS; i++) {
    if (!bullets[i].a) continue;
    for (int r = 0; r < ER; r++) {
      for (int c = 0; c < EC; c++) {
        if (!enemy[r][c]) continue;
        int x = ex + c * (ENEMY_W + ENEMY_GAP_X);
        int y = ey + r * (ENEMY_H + ENEMY_GAP_Y);
        if (bullets[i].x > x && bullets[i].x < x + ENEMY_W &&
bullets[i].y > y && bullets[i].y < y + ENEMY_H) {
          enemy[r][c] = false;
          bullets[i].a = false;
        }
      }
    }
  }
}
// ================= INTRO =================
void drawMonsterHead3D(int x, int y, int frame) {
  // pseudo-3D face on right side
  u8g2.drawRFrame(x, y, 28, 20, 3);
  u8g2.drawRFrame(x + 2, y + 2, 24, 16, 2);
  u8g2.drawDisc(x + 8, y + 8, 2, U8G2_DRAW_ALL);
  u8g2.drawDisc(x + 20, y + 8, 2, U8G2_DRAW_ALL);
  u8g2.drawLine(x + 6, y + 14, x + 22, y + 14);
  u8g2.drawLine(x + 3, y + 3, x - 2, y - 2);
  u8g2.drawLine(x + 25, y + 3, x + 30, y - 2);
  if (frame % 20 < 10) {
    u8g2.drawLine(x + 10, y + 15, x + 14, y + 17);
    u8g2.drawLine(x + 18, y + 15, x + 14, y + 17);
  } else {
    u8g2.drawLine(x + 9, y + 16, x + 19, y + 16);
  }
}
void drawIntro() {
  u8g2.clearBuffer();
  int frame = (millis() / 90) % 40;
  // left side text
  u8g2.setFont(u8g2_font_7x13B_tr);
  u8g2.drawStr(8, 14, "SPACE");
  u8g2.drawStr(8, 28, "INVADERS");
  u8g2.setFont(u8g2_font_5x8_tf);
  u8g2.drawStr(8, 39, "by Yaro");
  // right side monster face
  drawMonsterHead3D(88, 8, frame);
  // small battle scene lower area
  int invX = 16 + (frame % 18);
  drawEnemyType3(invX, 46, frame % 2 == 0);
  drawEnemyType2(invX + 18, 46, frame % 2 != 0);
  drawPlayer();
  if (frame % 16 < 8) {
    u8g2.drawVLine(px + 6, 44, 8);
  }
  u8g2.drawHLine(0, 54, 128);
  u8g2.setFont(u8g2_font_6x12_tr);
  u8g2.drawStr(28, 63, "PRESS B");
  u8g2.sendBuffer();
}
// ================= SCREENS =================
void drawHUD() {
  u8g2.setFont(u8g2_font_5x8_tf);
  u8g2.setCursor(0, 8);
  u8g2.print("LVL:");
  u8g2.print(level);
}
void drawLevelComplete() {
  u8g2.clearBuffer();
  u8g2.setFont(u8g2_font_6x12_tr);
  u8g2.drawStr(14, 20, "LEVEL COMPLETE");
  u8g2.setFont(u8g2_font_5x8_tf);
  u8g2.drawStr(22, 34, "Press B for the");
  u8g2.drawStr(32, 44, "next level");
  u8g2.drawStr(48, 56, "L=");
  u8g2.print(completedLevel);
  u8g2.sendBuffer();
}
void drawGameOver() {
  u8g2.clearBuffer();
  u8g2.setFont(u8g2_font_7x13B_tr);
  u8g2.drawStr(22, 22, "GAME OVER");
  u8g2.setFont(u8g2_font_5x8_tf);
  u8g2.drawStr(18, 38, "Invaders reached");
  u8g2.drawStr(35, 46, "the base");
  u8g2.drawStr(18, 58, "Press B to restart");
  u8g2.sendBuffer();
}
void render() {
  u8g2.clearBuffer();
  drawHUD();
  drawEnemies();
  drawBullets();
  drawPlayer();
  u8g2.sendBuffer();
}
// ================= SETUP =================
void setup() {
  Serial.begin(115200);
  Wire.begin();
  u8g2.begin();
  startGame();
}
// ================= LOOP =================
void loop() {
  update_touch();
  if (intro) {
    drawIntro();
    if (pressed(T_B)) {
      intro = false;
      startGame();
    }
    delay(30);
    return;
  }
  if (gameOver) {
    drawGameOver();
    if (pressed(T_B)) {
      startGame();
    }
    delay(30);
    return;
  }
  if (levelComplete) {
    drawLevelComplete();
    if (pressed(T_B)) {
      startNextLevel();
    }
    delay(30);
    return;
  }
  if (held(T_LEFT)) px -= 2;
  if (held(T_RIGHT)) px += 2;
  if (px < 0) px = 0;
  if (px > 116) px = 116;
  if (pressed(T_A)) fire();
  updateBullets();
  updateEnemies();
  checkHit();
  if (!enemiesAlive()) {
    completedLevel = level;
    levelComplete = true;
  }
  render();
  delay(30);
}
Space Invaders running on QPad display
Space Invaders running on QPad display.
Video 5 - Space Invaders game demo.

Reflection

References

Video References

Arduino Code References (.ino)